Guest User

Untitled

a guest
Aug 8th, 2023
46
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // ==UserScript==
  2. // @name        HeyParty
  3. // @namespace   Violentmonkey Scripts
  4. // @match       *://cgi.heyuri.net/party2/*
  5. // @grant       none
  6. // @version     1.0
  7. // @author      -
  8. // @description Improved interface for party2, heyuri edition
  9. // ==/UserScript==
  10.  
  11. let retryTimeout = undefined
  12.  
  13. const main = async () => {
  14.   if(document.querySelector(".loginTable")) {
  15.     return
  16.   } // do not run on the login page
  17.  
  18.   const SMALLEST_INTERVAL = 1000 // minimum time between two managed requests (comments and non-action commands like @$100slot)
  19.   const UPDATE_INTERVAL = 10000 // by request from anonwaha
  20.   const COMMENT_INTERVAL = SMALLEST_INTERVAL // minimum time between two comments
  21.   const TICK_INTERVAL = 16 // about 60 times per second
  22.   const SAFETY_MARGIN = 50 // 500ms safety margin because the server can be a little iffy otherwise
  23.  
  24.   // if we can't see the document yet, reschedule in 1 second and try again
  25.   if(!document.querySelector("#form")) {
  26.     if(retryTimeout) {
  27.       clearTimeout(retryTimeout)
  28.     }
  29.     retryTimeout = setTimeout(main, SMALLEST_INTERVAL + SAFETY_MARGIN)
  30.     return
  31.   } else {
  32.     clearTimeout(retryTimeout)
  33.     retryTimeout = undefined
  34.   }
  35.  
  36.   let rawVisible = false
  37.   let queuedAction = undefined
  38.   let queuedComments = []
  39.  
  40.   let origGage = window.active_gage
  41.   let origWake = window.wake_time
  42.   let origCD = window.count_down
  43.  
  44.   let doAction = true
  45.   let gaugeDone = true
  46.   let updatesEnabled = true
  47.  
  48.   let sleepTimer = undefined
  49.   let gaugeEngaged = false
  50.  
  51.   let prevTime = undefined
  52.   let prevCommentTime = undefined
  53.   let prevUpdateTime = undefined
  54.  
  55.   let latestRequest = undefined
  56.   let lastExtraStatus = undefined
  57.  
  58.   let tickInterval = undefined
  59.   let ticking = false
  60.  
  61.   let inCombat = false
  62.   let inDungeon = false
  63.  
  64.   let activeMenu = undefined
  65.  
  66.   const allElts = ["status", "room", "chatbox", "controls", "menu", "text-layout"]
  67.  
  68.   // Actions default to manual operations, i.e. just send the action to the chatbox and focus it...
  69.  
  70.   // Actions that are handled specially (e.g. requires multiple state updates to properly progress)
  71.   let specialActions = [
  72.     "@Home",
  73.     "@Invite",
  74.     "@Logout",
  75.     "@Sleep",
  76.     "@RunAway",
  77.     "@Profile",
  78.     "@MonsterBook",
  79.     "@ItemEncyclopedia",
  80.     "@JobMastery",
  81.     "@Proceed",
  82.     "@ReadLetter",
  83.     "@Guild",
  84.  
  85.     // Require double-updates in case a combat starts
  86.     // TODO: traps?
  87.     "@North",
  88.     "@South",
  89.     "@East",
  90.     "@West",
  91.   ]
  92.   // Actions that should instantly dispatch
  93.   let ordinaryActions = [
  94.     "@Guild",
  95.     "@Move",
  96.     "@Make",
  97.     "@Spectate",
  98.  
  99.     "@ChangeJob",
  100.     "@Order",
  101.     "@Withdraw",
  102.     "@Store",
  103.     "@Use",
  104.     "@Sell",
  105.     "@Buy",
  106.  
  107.     "@Screenshot",
  108.  
  109.     "@Map",
  110.  
  111.     "@Fap"
  112.   ]
  113.   // Actions that are only handled specially when a target is added to the command.
  114.   // This usually happens for menu actions (e.g. @Move summons the menu and @Move>Somewhere commits the actual action)
  115.   let specialActionsWhenTargeted = [
  116.     "@Move",
  117.  
  118.     "@Party", // TODO: potential special handling for arena @Party
  119.     "@Challenge",
  120.     "@Dungeon",
  121.     "@Arena",
  122.     "@GuildBattle"
  123.   ]
  124.   // Actions to always show in combat
  125.   let extraCombatActions = [
  126.     "@Proceed",
  127.   ]
  128.   // Actions that will show in the drop-down menu (only if part of the action set returned by the server)
  129.   // Note that in combat, any non-special, non-ordinary action is assumed to be a combat move and conceptually
  130.   // added to this list.
  131.   // Excluded options are in untargetedCombatActions.
  132.   let targetedActions = [
  133.     "@Speak",
  134.     "@Attack",
  135.     "@Examine",
  136.     "@Fap",
  137.     "@Kick"
  138.   ]
  139.   let untargetedCombatActions = [
  140.     "@West",
  141.     "@East",
  142.     "@North",
  143.     "@South",
  144.     "@Map",
  145.  
  146.     "@Start",
  147.     "@Party",
  148.  
  149.     "@Defend",
  150.     "@Tension"
  151.   ]
  152.   // These actions will fill the chatbox instead of being performed
  153.   // Mostly just for arithmetician...
  154.   let unhandledCombatActions = [
  155.     "@MP5",
  156.     "@MP4",
  157.     "@MP3"
  158.   ]
  159.   let currentActions = []
  160.  
  161.   const restyle = () => {
  162.     const rawRoot = document.querySelector(".raw")
  163.     if(rawRoot) {
  164.       const myRoot = document.querySelector(".hey")
  165.       rawRoot.style.display = rawVisible? "" : "none"
  166.       myRoot.style.display = rawVisible? "none": ""
  167.     }
  168.   }
  169.  
  170.   const autoWakeup = async () => {
  171.       await doRequest()
  172.       await doRequest()
  173.       const resp = await doRequest()
  174.       updateElements(resp)
  175.       doAction = true
  176.   }
  177.  
  178.   const updateTime = (nowTs) => {
  179.     let w_now_time = nowTs
  180.  
  181.     if (w_now_time >= 0) {
  182.       w_min  = Math.floor(w_now_time / 60);
  183.       w_sec  = Math.floor(w_now_time % 60);
  184.       w_sec  = ("00" + w_sec).substr(("00" + w_sec).length-2, 2);
  185.       w_nokori = w_min + 'm' + w_sec + 's';
  186.       let maybeWakeTime = document.querySelector("#wake_time")
  187.       if(maybeWakeTime) {
  188.         maybeWakeTime.innerHTML = w_nokori;
  189.       }
  190.  
  191.       if(sleepTimer) { clearTimeout(sleepTimer) }
  192.       sleepTimer = setTimeout(() => updateTime(nowTs-1), 1000)
  193.     }
  194.  
  195.     if(nowTs === 0) {
  196.       autoWakeup() // skip "Refreshed!" message
  197.     }
  198.   }
  199.  
  200.   const updateState = (resp) => {
  201.     // Use this function to update states/timers/triggers whenever a new page is fetched
  202.     let resetAction = true
  203.  
  204.     resp.querySelectorAll("script").forEach(s => {
  205.       let wake = s.textContent?.match(/.*wake_time\(\s*(-?\d+)\s*\).*/); // Find the one script that does wake_time...
  206.       if(wake) {
  207.         if(sleepTimer) { clearTimeout(sleepTimer) }
  208.         sleepTimer = setTimeout(() => updateTime(Number(wake[1])-1), 1000)
  209.       } // ... and dispatch it manually
  210.       const shouldRegauge = s.textContent?.match(/.*active_gage\(\s*(-?\d+)\s*,\s*(-?\d+)\s*\).*/)
  211.       if(shouldRegauge) {
  212.         const [now, target] = [Number(shouldRegauge[1]), Number(shouldRegauge[2])]
  213.         window.active_gage(now, target)
  214.         if(now >= 0) {
  215.           resetAction = false
  216.         }
  217.       }
  218.     })
  219.     if(resetAction) {
  220.       doAction = true
  221.     }
  222.   }
  223.  
  224.   const updateRaw = (rawRoot, queryDocument) => {
  225.     const frag = new DocumentFragment()
  226.     const newBtn = document.createElement("button")
  227.     const newBtnLbl = document.createTextNode("[New]")
  228.     newBtn.append(newBtnLbl)
  229.     newBtn.onclick = (e) => { rawVisible = !rawVisible; restyle(); }
  230.  
  231.     frag.append(newBtn)
  232.  
  233.     let copiedNodes = []
  234.     for(let n of queryDocument.children) {
  235.       copiedNodes.push(document.importNode(n, true))
  236.     }
  237.     copiedNodes.forEach(n => frag.append(n))
  238.  
  239.     rawRoot.replaceChildren(frag)
  240.   }
  241.  
  242.   const doRawRequest = async (payload) => {
  243.     if(latestRequest) {
  244.       const timeNow = new Date().getTime()
  245.       const timeOld = latestRequest
  246.       latestRequest = Math.max(new Date().getTime(), latestRequest) + SMALLEST_INTERVAL + SAFETY_MARGIN
  247.       await new Promise(r => setTimeout(r, Math.max(0, (SMALLEST_INTERVAL + SAFETY_MARGIN) - (timeNow - timeOld))))
  248.       latestRequest = new Date().getTime()
  249.     }
  250.  
  251.     const rawResp = await fetch("party.cgi",
  252.                         {
  253.                           method: "POST",
  254.                           headers: {
  255.                             'Content-Type': 'application/x-www-form-urlencoded'
  256.                           },
  257.                           body: new URLSearchParams(payload).toString()
  258.                         })
  259.     const respText = await rawResp.text()
  260.     const parser = new DOMParser()
  261.  
  262.     const resp = parser.parseFromString(respText, "text/html")
  263.     const raw = document.querySelector(".raw")
  264.     if(raw) {
  265.       updateRaw(raw, resp.body)
  266.     }
  267.  
  268.     latestRequest = new Date().getTime()
  269.  
  270.     return resp
  271.   }
  272.  
  273.   const doRequest = async (comment = "") => {
  274.     const { id, pass } = JSON.parse(localStorage.getItem("heypartyCreds"))
  275.     const fd = {}
  276.  
  277.     let brace = false
  278.     if(comment.match(/@Proceed(\s+|$)/)) { brace = true } // hack the game does when proceeding... required to not receive the pre-proceed update.
  279.  
  280.     fd['id'] = id
  281.     fd['pass'] = pass
  282.     fd['reload_time'] = "0"
  283.     fd['comment'] = comment
  284.  
  285.     let resp = await doRawRequest(fd)
  286.     if(brace) {
  287.       fd['comment'] = ""
  288.       resp = await doRawRequest(fd)
  289.     }
  290.  
  291.     return resp
  292.   }
  293.  
  294.   const parseLogsChats = (messages) => {
  295.     let chatMsgs = []
  296.     let logMsgs = []
  297.  
  298.     messages.forEach(m => {
  299.       const [speaker, text] = m.textContent.split(/:/, 2) // WARNING: THIS IS NOT IN FACT A COLON
  300.  
  301.       let words = text.split(/\s+/).filter(w => w.length > 0)
  302.       const timestamp = words.slice(-2)
  303.       words = words.slice(0, -2)
  304.       const isOnlyCommands = speaker.match(/^@System$/) || m.querySelector("span") || words.every(w => w[0] === "@" || w[0] === ">")
  305.       if(isOnlyCommands) {
  306.         logMsgs.push(m)
  307.         return;
  308.       }
  309.  
  310.       const isOnlySpeech = !words.some(w => w.match(/(@.+)/))
  311.       if(isOnlySpeech) {
  312.         chatMsgs.push(m)
  313.         return;
  314.       }
  315.  
  316.       chatMsgs.push(m)
  317.       logMsgs.push(m)
  318.     })
  319.  
  320.     return [logMsgs, chatMsgs]
  321.   }
  322.  
  323.   const updateStateDisplays = () => {
  324.     const notice = document.querySelector(".pending-action")
  325.     const commentNotice = document.querySelector(".pending-comment")
  326.     if(!notice || !commentNotice) { return; }
  327.  
  328.     if(!queuedAction) {
  329.       notice.innerHTML = ""
  330.       notice.style.display = "none"
  331.     } else {
  332.       notice.innerHTML = "Pending: " + queuedAction.action
  333.       notice.style.display = ""
  334.     }
  335.  
  336.     if(queuedComments.length > 0) {
  337.       commentNotice.innerHTML = `Comments:\n${queuedComments.join("\n")}`
  338.       commentNotice.style.display = ""
  339.     } else {
  340.       commentNotice.innerHTML = ""
  341.       commentNotice.style.display = "none"
  342.     }
  343.   }
  344.  
  345.   const queueAction = (action) => {
  346.     queuedAction = action
  347.     updateStateDisplays()
  348.   }
  349.  
  350.   const queueComment = (comment) => {
  351.     queuedComments.push(comment)
  352.     updateStateDisplays()
  353.   }
  354.  
  355.   const createMenu = (from) => {
  356.     const frag = new DocumentFragment()
  357.  
  358.     frag.replaceChildren(document.importNode(from, true))
  359.  
  360.     return frag
  361.   }
  362.  
  363.   const createAltMenu = (froms) => {
  364.     const frag = new DocumentFragment()
  365.  
  366.     froms.forEach(from => {
  367.       const node = document.importNode(from, true)
  368.       const actionString = node.onclick.toString().match(/text_set\((.*?)\)/)[1].slice(1, -2)
  369.       node.className = "alt-action"
  370.       node.removeAttribute("onclick")
  371.       node.onclick = async e => { queueAction({ action: actionString, type: 'action' }) }
  372.  
  373.       frag.append(node)
  374.     })
  375.  
  376.     return frag
  377.   }
  378.  
  379.   const requestCombat = async (tbl) => {
  380.     const fd = {}
  381.     if(!tbl) { return }
  382.     Array.from(tbl.querySelectorAll("input,select")).forEach(e => {
  383.       if(e.name && e.name.length > 0) {
  384.         fd[e.name] = e.value
  385.       }
  386.     })
  387.  
  388.     const { id, pass } = JSON.parse(localStorage.getItem("heypartyCreds")) ?? { id: "", pass: "" }
  389.  
  390.     fd['id'] = id
  391.     fd['pass'] = pass
  392.     fd['comment'] = tbl.querySelector('input[type="submit"]').value
  393.  
  394.     await doRawRequest(fd) // combat create is special and always requires an update...
  395.     return await doRequest()
  396.   }
  397.  
  398.   const inferCombatState = (resp) => {
  399.       const bgImg = resp.querySelector('div[style^="background"]')?.style.background?.match(/url\("(.*?)"\)/)
  400.       const maybeInDungeon = bgImg? bgImg[1].match(/map\d+\..*/) : false
  401.       const maybeInCombat = maybeInDungeon || (bgImg? bgImg[1].match(/(stage|challenge)\d+\..*/) : false) // Assume all maps have the form typeX and hardcode types...
  402.       return [maybeInCombat? true : false, maybeInDungeon? true : false] // transform the match/null into true/false...
  403.   }
  404.  
  405.   const updateElement = (queryDocument, elt, div) => {
  406.     if(!div) { return }
  407.     switch(elt) {
  408.       case "status":
  409.         const statusFrag = new DocumentFragment()
  410.         const rawMes = queryDocument.querySelector(".mes:not(.status)")
  411.  
  412.         const [combatStatus, dungeonStatus] = inferCombatState(queryDocument)
  413.         // setting directly by destructuring does not work and will instead replace
  414.         // the value of rawMes...
  415.         inCombat = combatStatus
  416.         inDungeon = dungeonStatus
  417.  
  418.         const extraStatus = queryDocument.querySelector(".strong")
  419.         const extraStatusIsMenu = extraStatus?.querySelector(".table1")
  420.         const altExtraStatusIsMenu = extraStatus?.querySelector("span[onclick]")
  421.  
  422.         let copiedMes = []
  423.         for(let n of rawMes.childNodes ?? []) {
  424.           copiedMes.push(document.importNode(n, true))
  425.         }
  426.         copiedMes.forEach(n => statusFrag.append(n))
  427.  
  428.         if(extraStatus && !extraStatusIsMenu && !altExtraStatusIsMenu) {
  429.           const thisStatus = document.importNode(extraStatus, true)
  430.           statusFrag.append(thisStatus)
  431.           lastExtraStatus = thisStatus
  432.         } else if(!extraStatus && lastExtraStatus) {
  433.           statusFrag.append(lastExtraStatus)
  434.           lastExtraStatus = undefined
  435.         }
  436.  
  437.         const notice = document.createElement("pre")
  438.         notice.className = "pending-action"
  439.         const commentNotice = document.createElement("pre")
  440.         commentNotice.className = "pending-comment"
  441.         const updateNotice = document.createElement("pre")
  442.         updateNotice.className = "update-notice"
  443.         const updateNoticeText = document.createTextNode("Next update: now")
  444.         updateNotice.append(updateNoticeText)
  445.         statusFrag.append(notice)
  446.         statusFrag.append(commentNotice)
  447.         statusFrag.append(updateNotice)
  448.         div.replaceChildren(statusFrag)
  449.         div.classList.add("mes")
  450.  
  451.         updateState(queryDocument)
  452.         updateStateDisplays()
  453.  
  454.         break;
  455.       case "room":
  456.         const roomFrag = new DocumentFragment()
  457.         const rawRoom = queryDocument.querySelector(".view") // party view
  458.         if(rawRoom) {
  459.           const roomView = document.importNode(rawRoom, true)
  460.           div.append(roomView)
  461.           const room = document.importNode(rawRoom.nextSibling, true)
  462.           roomFrag.append(room) // actual room display
  463.         } else {
  464.           const combatRoom = queryDocument.querySelector(".mes").nextSibling
  465.           const room = document.importNode(combatRoom, true)
  466.           roomFrag.append(room) // actual room display
  467.         }
  468.         if(roomFrag.lastChild && roomFrag.lastChild.querySelector) {
  469.           const maybeSelectable = roomFrag.lastChild.querySelector("table")
  470.           if(maybeSelectable) {
  471.             const selectables = maybeSelectable.querySelectorAll("td")
  472.             selectables.forEach(s => {
  473.               s.className = "selectable"
  474.               const menu = document.createElement("div")
  475.               menu.className = "selectable-menu"
  476.               currentActions.concat("@Fap").forEach(a => {
  477.                 if((inCombat && !specialActions.some(act => act === a) && !ordinaryActions.some(act => act === a) && !untargetedCombatActions.some(act => act === a))
  478.                    || targetedActions.some(act => act === a) || a === "@Fap") {
  479.                   const menuItem = document.createElement("div")
  480.                   menuItem.className = "selectable-menu-item"
  481.                   const txt = document.createTextNode(a)
  482.                   menuItem.append(txt)
  483.                   menuItem.value = a + s.onclick.toString().match(/text_set\((.*?)\)/)[1].slice(1, -2)
  484.                   menuItem.removeAttribute("onclick")
  485.                   menuItem.onclick = (e) => { queueAction({ action: e.target.value, type: 'action' }) }
  486.                   menu.append(menuItem)
  487.                 }
  488.               })
  489.  
  490.               s.removeAttribute("onclick")
  491.               s.append(menu)
  492.             })
  493.           }
  494.         }
  495.         div.replaceChildren(roomFrag)
  496.         break;
  497.       case "chatbox":
  498.         const chatboxFrag = new DocumentFragment()
  499.  
  500.         const chatLayout = document.createElement("div")
  501.         chatLayout.className = "chat-layout"
  502.  
  503.         const chatField = document.createElement("input")
  504.         chatField.type = "text"
  505.         chatField.className = "ipt-chat"
  506.  
  507.         const chatSend = document.createElement("button")
  508.         chatSend.className = "btn-send"
  509.         chatSend.append(document.createTextNode("Send"))
  510.         chatSend.onclick = (e) => {
  511.           if(chatField.value.match(/@.+(\s+|$|>)/)) {
  512.             queueAction({ action: chatField.value, type: 'literal' })
  513.           } else {
  514.             queueComment(chatField.value)
  515.           }
  516.  
  517.           chatField.value = ""
  518.         }
  519.  
  520.         chatField.addEventListener("keyup",
  521.                                    (e) => {
  522.                                           e.preventDefault();
  523.                                           if (e.keyCode === 13) {
  524.                                             chatSend.click()
  525.                                           }
  526.         })
  527.  
  528.         chatLayout.append(chatField)
  529.         chatLayout.append(chatSend)
  530.  
  531.         chatboxFrag.append(chatLayout)
  532.  
  533.         div.replaceChildren(chatboxFrag)
  534.  
  535.         break;
  536.       case "controls":
  537.         const controlsFrag = new DocumentFragment()
  538.  
  539.         const actions = queryDocument.querySelectorAll(".actionLink")
  540.         const actionsLayout = document.createElement("div")
  541.         actionsLayout.className = 'actions-layout'
  542.  
  543.         let currentRow = 1
  544.  
  545.         currentActions = []
  546.  
  547.         actions.forEach(a => {
  548.           if(["@Move", "@Attack", "@Use"].some(w => a.textContent === w)) {
  549.             currentRow++
  550.           }
  551.  
  552.           const btn = document.createElement("button")
  553.           btn.onclick = (e) => { queueAction({ action: e.target.value, type: 'action' }) }
  554.           btn.value = a.textContent
  555.           const lbl = document.createTextNode(btn.value)
  556.  
  557.           currentActions.push(btn.value)
  558.  
  559.           btn.append(lbl)
  560.           btn.className = 'btn-action'
  561.           btn.style['grid-row-start'] = currentRow
  562.  
  563.           actionsLayout.append(btn)
  564.         })
  565.  
  566.         currentRow++
  567.  
  568.         if(inCombat) {
  569.           extraCombatActions.forEach(a => {
  570.             const combatBtn = document.createElement("button")
  571.             combatBtn.onclick = (e) => { queueAction({ action: a, type: 'action' }) }
  572.             combatBtn.value = a
  573.             const combatLbl = document.createTextNode(a)
  574.             currentActions.push(a)
  575.  
  576.             combatBtn.append(combatLbl)
  577.             combatBtn.className = 'btn-action'
  578.             combatBtn.style['grid-row-start'] = currentRow
  579.  
  580.             actionsLayout.append(combatBtn)
  581.           })
  582.           currentRow++
  583.         }
  584.  
  585.         if(inDungeon) {
  586.           const glyphs = { "@North": "^", "@South": "v", "@East": ">", "@West": "<" }
  587.           const positions = { "@North": [1, 2], "@South": [3, 2], "@East": [2, 3], "@West": [2, 1]}
  588.           const dirLayout = document.createElement("div")
  589.           dirLayout.className = "dir-container"
  590.           Object.keys(glyphs).forEach(dir => {
  591.             const btn = document.createElement("button")
  592.             btn.className = "btn-dir"
  593.             const btnLbl = document.createTextNode(glyphs[dir])
  594.             btn.append(btnLbl)
  595.             btn.onclick = e => { queueAction({ action: dir, type: 'action' }) }
  596.             btn.style['grid-row-start'] = positions[dir][0]
  597.             btn.style['grid-column-start'] = positions[dir][1]
  598.             dirLayout.append(btn)
  599.           })
  600.  
  601.           dirLayout.style['grid-row-start'] = currentRow
  602.           actionsLayout.append(dirLayout)
  603.  
  604.           currentRow++
  605.         }
  606.  
  607.         const rawBtn = document.createElement("button")
  608.         const rawBtnLbl = document.createTextNode("[Original]")
  609.  
  610.         rawBtn.className = "btn-raw"
  611.         rawBtn.append(rawBtnLbl)
  612.  
  613.         rawBtn.onclick = (e) => { rawVisible = !rawVisible; restyle(); }
  614.  
  615.         rawBtn.style['grid-row-start'] = currentRow
  616.  
  617.         actionsLayout.append(rawBtn)
  618.  
  619.         controlsFrag.append(actionsLayout)
  620.         div.replaceChildren(controlsFrag)
  621.  
  622.         break;
  623.       case "text-layout":
  624.         const textFrag = new DocumentFragment()
  625.  
  626.         const [logs, chats] = parseLogsChats(queryDocument.querySelectorAll(".message"))
  627.  
  628.         const chatDiv = document.createElement("div")
  629.         chatDiv.className = "chat"
  630.         const chatHeader = document.createElement("div")
  631.         chatHeader.className = "chat-header"
  632.         const chatLbl = document.createTextNode("Chat")
  633.         chatHeader.append(chatLbl)
  634.         chatDiv.append(chatHeader)
  635.  
  636.         const logDiv = document.createElement("div")
  637.         logDiv.className = "log"
  638.         const logHeader = document.createElement("div")
  639.         logHeader.className = "log-header"
  640.         const logLbl = document.createTextNode("Log")
  641.         logHeader.append(logLbl)
  642.         logDiv.append(logHeader)
  643.  
  644.         textFrag.append(chatDiv)
  645.         textFrag.append(logDiv)
  646.  
  647.         chats.forEach(cq => {
  648.           const c = document.importNode(cq, true)
  649.           const chatCard = document.createElement("div")
  650.           chatCard.className = "message-card"
  651.           chatCard.append(c)
  652.           chatDiv.append(chatCard)
  653.         })
  654.         logs.forEach(cq => {
  655.           const c = document.importNode(cq, true)
  656.           const logCard = document.createElement("div")
  657.           logCard.className = "log-card"
  658.           logCard.append(c)
  659.           logDiv.append(logCard)
  660.         })
  661.  
  662.         div.replaceChildren(textFrag)
  663.  
  664.         break;
  665.  
  666.       case "menu":
  667.         const extra = queryDocument.querySelector(".strong")
  668.         const extraStatusMenu = extra?.querySelectorAll(".table1")
  669.         const altExtraStatusMenu = extra?.querySelectorAll("span[onclick]")
  670.  
  671.         const invalid = extra?.textContent.includes("Go to") // Game does a hack with a never-seen intermediate page with this text in the menu section
  672.  
  673.         if((invalid || !altExtraStatusMenu) && (!extraStatusMenu || extraStatusMenu.length === 0)) {
  674.           if(activeMenu) {
  675.             div.replaceChildren(activeMenu)
  676.           } else {
  677.             div.replaceChildren(new DocumentFragment())
  678.           }
  679.           activeMenu = undefined
  680.           return
  681.         }
  682.  
  683.         const menuFrag = new DocumentFragment()
  684.  
  685.         if(extraStatusMenu && extraStatusMenu.length > 0) {
  686.           if(extraStatusMenu.length > 1) {
  687.             let menus = []
  688.  
  689.             const sel = document.createElement("select")
  690.  
  691.             menuFrag.append(sel)
  692.  
  693.             extraStatusMenu.forEach((e, i) => {
  694.               const o = document.createElement("option")
  695.               const submitMenu = e.querySelector('input[type="submit"]')?.value
  696.               if(submitMenu) {
  697.                 o.value = submitMenu
  698.               } else {
  699.                 o.value = `Menu ${i+1}`
  700.               }
  701.  
  702.               const optionLbl = document.createTextNode(o.value)
  703.               o.append(optionLbl)
  704.               sel.append(o)
  705.  
  706.               const menuDiv = document.createElement("div")
  707.               menuDiv.className = "menu-container"
  708.  
  709.               menuDiv.replaceChildren(createMenu(e))
  710.  
  711.               const partyData = JSON.parse(localStorage.getItem("heypartyParties"))
  712.               const thisPartyData = (partyData && partyData[submitMenu]) ?? Object.fromEntries([[submitMenu, {}]])
  713.               partyData[submitMenu] = thisPartyData
  714.  
  715.               for(let [k, v] of Object.entries(thisPartyData)) {
  716.                 const maybeParty = menuDiv.querySelector(`.text_box1[name="${k}"],.select1[name="${k}"]`)
  717.                 if(maybeParty) {
  718.                   maybeParty.value = thisPartyData[k] ?? ""
  719.                 }
  720.               }
  721.  
  722.               const menuSubmitButton = menuDiv.querySelector('input[type="submit"]')
  723.               const partyField = menuDiv.querySelector('.text_box1[name="p_name"]')
  724.  
  725.               partyField.addEventListener("keyup",
  726.                            (e) => {
  727.                                   e.preventDefault();
  728.                                   if (e.keyCode === 13) {
  729.                                     menuSubmitButton.click()
  730.                                   }
  731.               })
  732.  
  733.               if(partyField && menuSubmitButton) {
  734.                 menuSubmitButton.onclick = async (evt) => {
  735.                   evt.preventDefault()
  736.                   activeMenu = undefined // close the menu
  737.  
  738.                   Array.from(menuDiv.querySelectorAll('.text_box1[name],.select1[name]')).forEach(e => partyData[submitMenu][e.name] = e.value)
  739.                   localStorage.setItem("heypartyParties", JSON.stringify(partyData))
  740.  
  741.                   const resp = await requestCombat(menuDiv.querySelector(".table1"))
  742.                   const [maybeInCombat, maybeInDungeon] = inferCombatState(resp)
  743.                   if(resp && maybeInCombat) { // success
  744.                     await doRequest() // create update, i.e. enter the quest
  745.                     for(let elt of allElts) {
  746.                       updateElement(resp, elt, document.querySelector("." + elt))
  747.                     }
  748.                   } else {
  749.                     // failure, "error" status will need an update
  750.                     updateElement(resp, "status", document.querySelector(".status"))
  751.                     updateElement(resp, "menu", document.querySelector(".menu"))
  752.                   }
  753.                 }
  754.               }
  755.  
  756.               menus[o.value] = menuDiv
  757.  
  758.               menuFrag.append(menuDiv)
  759.             })
  760.  
  761.             sel.onchange = (e) => {
  762.               Array.from(Object.entries(menus)).forEach(([k,div]) => div.style.display = "none")
  763.               menus[e.target.value].style.display = ""
  764.             }
  765.  
  766.             sel.selectedIndex = 0
  767.  
  768.             sel.dispatchEvent(new Event("change"))
  769.  
  770.             doAction = true
  771.  
  772.           } else if(extraStatusMenu) {
  773.             console.log("One menu path", extraStatusMenu)
  774.             const menuDiv = document.createElement("div")
  775.             menuDiv.className = "menu-container"
  776.  
  777.             menuDiv.replaceChildren(createMenu(extraStatusMenu[0]))
  778.  
  779.             menuFrag.append(menuDiv)
  780.           }
  781.  
  782.           activeMenu = menuFrag
  783.           div.replaceChildren(menuFrag)
  784.  
  785.           doAction = true
  786.  
  787.         } else if(altExtraStatusMenu) {
  788.           const menuDiv = document.createElement("div")
  789.           menuDiv.className = "menu-container strong"
  790.           const altMenu = createAltMenu(altExtraStatusMenu)
  791.           menuDiv.append(document.importNode(extra.childNodes[0])) // text caption for the action
  792.           menuDiv.append(document.createElement("br"))
  793.           const actualMenuDiv = document.createElement("div")
  794.           actualMenuDiv.className = "alt-menu"
  795.           actualMenuDiv.append(altMenu)
  796.           menuDiv.append(actualMenuDiv)
  797.  
  798.           menuFrag.append(menuDiv)
  799.  
  800.           activeMenu = menuFrag
  801.           div.replaceChildren(menuFrag)
  802.  
  803.           doAction = true
  804.         }
  805.       break;
  806.     }
  807.   }
  808.  
  809.   let updateElements = (resp, which) => {
  810.     for(let elt of (which ?? allElts)) {
  811.       updateElement(resp, elt, document.querySelector("." + elt))
  812.     }
  813.   }
  814.  
  815.   const handleAction = async () => {
  816.     if(!queuedAction) { return }
  817.     const { action, type } = queuedAction
  818.  
  819.     lastExtraStatus = undefined
  820.     activeMenu = undefined
  821.  
  822.     let trueAction = action.match(/.*?(@.+?)(\s+|$|>)/)
  823.     let actionIsTargeted = action.match(/.*?(>.+?)(\s+|$|@)/)
  824.  
  825.     queuedAction = undefined
  826.     if(trueAction) {
  827.       trueAction = trueAction[1]
  828.     }
  829.  
  830.     if(type === "literal") {
  831.       const resp = await doRequest(action)
  832.       updateElements(resp)
  833.       return
  834.     }
  835.  
  836.     if(actionIsTargeted) {
  837.       if(specialActionsWhenTargeted.some(a => a === trueAction)) {
  838.         let unhandled = false;
  839.         switch(trueAction) {
  840.           case "@Move":
  841.           case "@Party":
  842.           case "@Dungeon":
  843.           case "@GuildBattle":
  844.           case "@Challenge":
  845.           case "@Arena":
  846.             await doRequest(action)
  847.             const resp = await doRequest() // update page after the action that requires double-updates
  848.             activeMenu = undefined
  849.             updateElements(resp)
  850.             break;
  851.  
  852.           default:
  853.             unhandled = true;
  854.         }
  855.  
  856.         if(!unhandled) {
  857.           return
  858.         }
  859.       }
  860.     }
  861.  
  862.     if(specialActions.some(a => a === trueAction)) {
  863.       let unhandled = false;
  864.       let resp = undefined;
  865.  
  866.       switch(trueAction) {
  867.         // These actions will disable updates and show the raw page (with a special button to cancel)
  868.         case "@Invite":
  869.         case "@MonsterBook":
  870.         case "@Profile":
  871.         case "@ItemEncyclopedia":
  872.         case "@JobMastery":
  873.           await doRequest(action)
  874.           const specResp = await doRequest()
  875.  
  876.           const oldBody = new DocumentFragment()
  877.           Array.from(document.querySelector(".raw").children).forEach(c => oldBody.append(c))
  878.  
  879.           document.querySelector(".raw").replaceChildren(document.importNode(specResp.body, true))
  880.  
  881.           const backButton = document.createElement("button")
  882.           backButton.className = "btn-back-from-special"
  883.           const backButtonLbl = document.createTextNode("[Back]")
  884.           backButton.append(backButtonLbl)
  885.           backButton.onclick = e => { raw.replaceChildren(oldBody); rawVisible = false; restyle(); updatesEnabled = true }
  886.  
  887.           const raw = document.querySelector(".raw")
  888.  
  889.           raw.querySelector("input[value='Return']")?.remove() // remove the 'return' button that goes back to the raw page
  890.           raw.insertBefore(backButton, raw.firstChild)
  891.  
  892.           updatesEnabled = false
  893.           rawVisible = true
  894.           restyle()
  895.           break;
  896.  
  897.         // These actions function normally, but require double updates to properly progress
  898.         // This is either to skip the "useless" "@X -> you did X! [Next]" screen,
  899.         // or for those that have 'transitory states' that are never actually seen in the original UI
  900.         // because they automatically generate a 2nd update...
  901.         case "@Home":
  902.         case "@RunAway":
  903.         case "@Sleep":
  904.         case "@Proceed":
  905.         case "@Guild":
  906.  
  907.         case "@North":
  908.         case "@South":
  909.         case "@East":
  910.         case "@West":
  911.           await doRequest(action)
  912.           resp = await doRequest() // update page after the action that requires double-updates
  913.           updateElements(resp)
  914.           break;
  915.  
  916.         // These actions simply disable updates until canceled.
  917.         case "@ReadLetter":
  918.           updatesEnabled = false
  919.  
  920.           resp = await doRequest(action)
  921.           updateElements(resp)
  922.  
  923.           const endButton = document.createElement("button")
  924.           endButton.className = "btn-back-from-special"
  925.           const endButtonLbl = document.createTextNode("[Back]")
  926.           endButton.append(endButtonLbl)
  927.           endButton.onclick = async (e) => { updatesEnabled = true; const resp = await doRequest(); updateElements(resp); e.target.remove() }
  928.  
  929.           const statusMenu = document.querySelector(".menu-container.strong")
  930.           statusMenu.lastElementChild.after(endButton)
  931.  
  932.           break;
  933.  
  934.         default:
  935.           unhandled = true
  936.           break;
  937.       }
  938.  
  939.       if(!unhandled) {
  940.         return
  941.       }
  942.     }
  943.  
  944.     // Assume all non-listed move are skills while we are in combat...
  945.     if((inCombat && !unhandledCombatActions.some(a => a === trueAction)) || (!inCombat && ordinaryActions.some(a => a === trueAction))) {
  946.       let resp = await doRequest(action)
  947.       if(inDungeon) {
  948.         // Killing an enemy immediately moves to the map view without
  949.         // requiring a 'proceed', so we have to account for that in dungeons
  950.         resp = await doRequest()
  951.       }
  952.       updateElements(resp)
  953.       return
  954.     }
  955.  
  956.     const chatBox = document.querySelector(".ipt-chat")
  957.     if(chatBox) {
  958.       chatBox.value = action
  959.       chatBox.focus()
  960.     }
  961.   }
  962.  
  963.   const tickFunction = async () => {
  964.     const root = document.querySelector(".heyparty")
  965.  
  966.     if(ticking || !root) { return; } // waiting for override() to get called
  967.  
  968.     ticking = true
  969.  
  970.     let doUpdate = false
  971.  
  972.     const currTime = new Date().getTime()
  973.  
  974.     if(prevTime) {
  975.       if(currTime - prevTime < TICK_INTERVAL) {
  976.         ticking = false
  977.         return
  978.       } else if(currTime - (prevUpdateTime ?? 0) >= UPDATE_INTERVAL + SAFETY_MARGIN) {
  979.         if(!sleepTimer) { // don't do updates when we're asleep
  980.           doUpdate = true
  981.         }
  982.       }
  983.     }
  984.  
  985.     if(doAction && gaugeDone && queuedAction) {
  986.       await new Promise(r => setTimeout(r, SAFETY_MARGIN))
  987.       await handleAction()
  988.     } else if(queuedComments.length > 0 && (!prevCommentTime || (currTime - prevCommentTime >= COMMENT_INTERVAL + SAFETY_MARGIN))) {
  989.       const comment = queuedComments.shift()
  990.       const resp = await doRequest(comment)
  991.       updateElements(resp, ["status", "text-layout", "room"])
  992.       prevCommentTime = new Date().getTime()
  993.     } else if(doUpdate && updatesEnabled) {
  994.         const resp = await doRequest()
  995.         updateElements(resp, ["text-layout", "room", "status"])
  996.         prevUpdateTime = new Date().getTime()
  997.     }
  998.  
  999.     prevTime = new Date().getTime()
  1000.  
  1001.     ticking = false
  1002.   }
  1003.  
  1004.   const override = async () => {
  1005.     const myRoot = document.createElement("div")
  1006.     myRoot.className = "heyparty"
  1007.  
  1008.     const hey = document.createElement("div")
  1009.     hey.className = "hey"
  1010.  
  1011.     // STYLE STYLE STYLE
  1012.     const myRootStyle = document.createElement("style")
  1013.     const styleContents = document.createTextNode(`
  1014.                             .hey {
  1015.  
  1016.                             }
  1017.  
  1018.                             .raw {
  1019.  
  1020.                             }
  1021.  
  1022.                             .dir-container {
  1023.                               display: grid;
  1024.                               grid-template-columns: 1fr 1fr 1fr;
  1025.                               grid-template-rows: 1fr 1fr 1fr;
  1026.                               max-width: 128px;
  1027.                               max-height: 128px;
  1028.                             }
  1029.  
  1030.                             .chat-layout {
  1031.                               display: flex;
  1032.                               flex-direction: row;
  1033.                             }
  1034.  
  1035.                             .actions-layout {
  1036.                               display: grid;
  1037.                               grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
  1038.                               grid-auto-flow: row;
  1039.                               row-gap: 1ch;
  1040.                               max-width: 256px;
  1041.                               justify-items: start;
  1042.                               overflow: visible;
  1043.                             }
  1044.  
  1045.                             .chat {
  1046.                             display: inline-block;
  1047.                             padding-left: 10px;
  1048.                             padding-right: 10px;
  1049.                             width: 40%;
  1050.                             }
  1051.  
  1052.                             .log {
  1053.                             display: inline-block;
  1054.                             padding-left: 10px;
  1055.                             padding-right: 10px;
  1056.                             width: 40%;
  1057.                             }
  1058.  
  1059.                             .text-layout {
  1060.                             display: flex;
  1061.                             flex-direction: row;
  1062.                             align-items: start;
  1063.                             }
  1064.  
  1065.                             .selectable:not(:hover) .selectable-menu {
  1066.                               display: none;
  1067.                             }
  1068.  
  1069.                             .selectable-menu {
  1070.                               position: absolute;
  1071.                               display: inline;
  1072.                               z-index: 1;
  1073.                               background: #336;
  1074.                               border: 1px white solid;
  1075.                             }
  1076.  
  1077.                             .selectable:hover .selectable-menu {
  1078.                               display: inline;
  1079.                             }
  1080.  
  1081.                             .selectable-menu:hover {
  1082.                               display: inline;
  1083.                             }
  1084.  
  1085.                             .selectable-menu-item:hover {
  1086.                               background: #666;
  1087.                               cursor: pointer;
  1088.                             }
  1089.  
  1090.                             .menu-container {
  1091.                               height: 256px;
  1092.                               overflow: auto;
  1093.                             }
  1094.  
  1095.                             .btn-action {
  1096.                             border: none;
  1097.                             background: none;
  1098.                             color: white;
  1099.                             cursor: pointer;
  1100.                             font-weight: bold;
  1101.                             font-size: 1em;
  1102.                             }
  1103.  
  1104.                             .chat-header {
  1105.                             margin-top: 0.5em;
  1106.                             margin-bottom: 0.5em;
  1107.                             font-size: 1.5rem;
  1108.                             font-weight: bold;
  1109.                             color: white;
  1110.                             border-style: dashed none dashed none;
  1111.                             }
  1112.  
  1113.                             .log-header {
  1114.                             margin-top: 0.5em;
  1115.                             margin-bottom: 0.5em;
  1116.                             font-size: 1.5rem;
  1117.                             font-weight: bold;
  1118.                             color: white;
  1119.                             border-style: dashed none dashed none;
  1120.                             }
  1121.  
  1122.                             .alt-menu {
  1123.                               display: flex;
  1124.                               flex-direction: row;
  1125.                               justify-content: start;
  1126.                               text-align: left;
  1127.                               align-items: flex-start;
  1128.                               flex-wrap: wrap;
  1129.                               max-width: 512px;
  1130.                             }
  1131.  
  1132.                             .alt-action {
  1133.                               margin-left: 3px;
  1134.                               margin-right: 3px;
  1135.                               margin-top: 3px;
  1136.                               margin-bottom: 3px;
  1137.                               cursor: pointer;
  1138.                             }
  1139.                             `)
  1140.     myRootStyle.append(styleContents)
  1141.  
  1142.     let allEltsDiv = []
  1143.  
  1144.     for(let elt of allElts) {
  1145.       const div = document.createElement("div")
  1146.       div.className = elt
  1147.       allEltsDiv.push(div)
  1148.     }
  1149.  
  1150.     for(let idx in allElts) {
  1151.       updateElement(document, allElts[idx], allEltsDiv[idx])
  1152.     }
  1153.  
  1154.     allEltsDiv.forEach(div => hey.append(div))
  1155.  
  1156.     const rawRoot = document.createElement("div")
  1157.  
  1158.     rawRoot.className = "raw"
  1159.     rawRoot.style.display = "none"
  1160.  
  1161.     myRoot.append(hey)
  1162.     myRoot.append(rawRoot)
  1163.  
  1164.     const myBody = document.createElement("body")
  1165.     myBody.append(myRoot)
  1166.  
  1167.     const form = document.querySelector("#form")
  1168.     const id = form.id.value
  1169.     const pass = form.pass.value
  1170.  
  1171.     localStorage.setItem("heypartyCreds", JSON.stringify({ id, pass }))
  1172.  
  1173.     updateRaw(rawRoot, document.body)
  1174.  
  1175.     document.body = myBody
  1176.     document.head.append(myRootStyle)
  1177.  
  1178.     window.count_down = (nowTs) => {
  1179.       origCD(nowTs)
  1180.     }
  1181.  
  1182.     window.wake_time = (nowTs) => { }
  1183.  
  1184.     window.active_gage = (nowTs, targetTs) => {
  1185.       const up = document.querySelector(".update-notice")
  1186.       if(nowTs > 0) {
  1187.         gaugeEngaged = true
  1188.         up.innerHTML = ("Next update: " + (nowTs) + "s")
  1189.  
  1190.         gaugeDone = false
  1191.         doAction = false
  1192.       }
  1193.       if(nowTs === 0) {
  1194.         gaugeEngaged = false
  1195.         up.innerHTML = "Next update: now"
  1196.  
  1197.         gaugeDone = true
  1198.         doAction = true
  1199.       }
  1200.  
  1201.       origGage(nowTs, targetTs)
  1202.     }
  1203.  
  1204.     if(tickInterval) {
  1205.       clearInterval(tickInterval)
  1206.       tickInterval = undefined
  1207.     }
  1208.     if(!tickInterval) {
  1209.       tickInterval = setInterval(tickFunction, TICK_INTERVAL)
  1210.     }
  1211.   }
  1212.  
  1213.   if(!document.querySelector(".heyparty")) {
  1214.     await override()
  1215.   }
  1216. }
  1217.  
  1218. try {
  1219.   main()
  1220. } catch(e) {
  1221.   console.log("Failed", e)
  1222. }
Advertisement
Add Comment
Please, Sign In to add comment