Guest User

Untitled

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