Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name HeyParty
- // @namespace Violentmonkey Scripts
- // @match *://cgi.heyuri.net/party2/*
- // @grant none
- // @version 1.0
- // @author -
- // @description Improved interface for party2, heyuri edition
- // ==/UserScript==
- let retryTimeout = undefined
- const SMALLEST_INTERVAL = 1000 // minimum time between two managed requests (comments and non-action commands like @$100slot)
- const UPDATE_INTERVAL = 10000 // by request from anonwaha
- const COMMENT_INTERVAL = SMALLEST_INTERVAL // minimum time between two comments
- const TICK_INTERVAL = 16 // about 60 times per second
- const SAFETY_MARGIN = 50 // 500ms safety margin because the server can be a little iffy otherwise
- const main = async () => {
- if(document.querySelector(".loginTable") || !document.querySelector('div[style^="background"]')) {
- return
- } // do not run on the login page, avoid trying to run until we see the background on a page
- // -- probably the reason for massive double-click pwnage when entering from the login page...
- // if we can't see the document yet, reschedule in SMALLEST_INTERVAL seconds and try again
- if(!document.querySelector("#form")) {
- if(retryTimeout) {
- clearTimeout(retryTimeout)
- }
- retryTimeout = setTimeout(main, SMALLEST_INTERVAL + SAFETY_MARGIN)
- return
- } else {
- clearTimeout(retryTimeout)
- retryTimeout = undefined
- }
- let rawVisible = false
- let queuedAction = undefined
- let queuedComments = []
- let origGage = window.active_gage
- let origWake = window.wake_time
- let origCD = window.count_down
- let doAction = true
- let gaugeDone = true
- let updatesEnabled = true
- let sleepTimer = undefined
- let gaugeEngaged = false
- let prevTime = undefined
- let prevCommentTime = undefined
- let prevUpdateTime = undefined
- let latestRequest = undefined
- let lastExtraStatus = undefined
- let tickInterval = undefined
- let ticking = false
- let inCombat = false
- let inDungeon = false
- let activeMenu = undefined
- const allElts = ["status", "room", "chatbox", "controls", "menu", "text-layout"]
- // Actions default to manual operations, i.e. just send the action to the chatbox and focus it...
- // Actions that are handled specially (e.g. requires multiple state updates to properly progress)
- let specialActions = [
- "@Home",
- "@Invite",
- "@Logout",
- "@Sleep",
- "@RunAway",
- "@Profile",
- "@MonsterBook",
- "@ItemEncyclopedia",
- "@JobMastery",
- "@Proceed",
- "@ReadLetter",
- "@Guild",
- // Require double-updates in case a combat starts
- // TODO: traps?
- "@North",
- "@South",
- "@East",
- "@West",
- "@Logout",
- ]
- // Actions that should instantly dispatch
- let ordinaryActions = [
- "@Guild",
- "@Move",
- "@Make",
- "@Spectate",
- "@ChangeJob",
- "@Order",
- "@Withdraw",
- "@Store",
- "@Use",
- "@Sell",
- "@Buy",
- "@Screenshot",
- "@Map",
- "@Fap"
- ]
- // Actions that are only handled specially when a target is added to the command.
- // This usually happens for menu actions (e.g. @Move summons the menu and @Move>Somewhere commits the actual action)
- let specialActionsWhenTargeted = [
- "@Move",
- "@Party", // TODO: potential special handling for arena @Party
- "@Challenge",
- "@Dungeon",
- "@Arena",
- "@GuildBattle"
- ]
- // Actions to always show in combat
- let extraCombatActions = [
- "@Proceed",
- ]
- // Actions that will show in the drop-down menu (only if part of the action set returned by the server)
- // Note that in combat, any non-special, non-ordinary action is assumed to be a combat move and conceptually
- // added to this list.
- // Excluded options are in untargetedCombatActions.
- let targetedActions = [
- "@Speak",
- "@Attack",
- "@Examine",
- "@Fap",
- "@Kick"
- ]
- let untargetedCombatActions = [
- "@West",
- "@East",
- "@North",
- "@South",
- "@Map",
- "@Start",
- "@Party",
- "@Defend",
- "@Tension"
- ]
- // These actions will fill the chatbox instead of being performed
- // Mostly just for arithmetician...
- let unhandledCombatActions = [
- "@MP5",
- "@MP4",
- "@MP3"
- ]
- let currentActions = []
- const restyle = () => {
- const rawRoot = document.querySelector(".raw")
- if(rawRoot) {
- const myRoot = document.querySelector(".hey")
- rawRoot.style.display = rawVisible? "" : "none"
- myRoot.style.display = rawVisible? "none": ""
- }
- }
- const autoWakeup = async () => {
- await doRequest()
- await doRequest()
- const resp = await doRequest()
- updateElements(resp)
- doAction = true
- }
- const updateTime = (nowTs) => {
- let w_now_time = nowTs
- if (w_now_time >= 0) {
- w_min = Math.floor(w_now_time / 60);
- w_sec = Math.floor(w_now_time % 60);
- w_sec = ("00" + w_sec).substr(("00" + w_sec).length-2, 2);
- w_nokori = w_min + 'm' + w_sec + 's';
- let maybeWakeTime = document.querySelector("#wake_time")
- if(maybeWakeTime) {
- maybeWakeTime.innerHTML = w_nokori;
- }
- if(sleepTimer) { clearTimeout(sleepTimer) }
- sleepTimer = setTimeout(() => updateTime(nowTs-1), 1000)
- }
- if(nowTs === 0) {
- autoWakeup() // skip "Refreshed!" message
- }
- }
- const updateState = (resp) => {
- // Use this function to update states/timers/triggers whenever a new page is fetched
- let resetAction = true
- resp.querySelectorAll("script").forEach(s => {
- let wake = s.textContent?.match(/.*wake_time\(\s*(-?\d+)\s*\).*/); // Find the one script that does wake_time...
- if(wake) {
- if(sleepTimer) { clearTimeout(sleepTimer) }
- sleepTimer = setTimeout(() => updateTime(Number(wake[1])-1), 1000)
- } // ... and dispatch it manually
- const shouldRegauge = s.textContent?.match(/.*active_gage\(\s*(-?\d+)\s*,\s*(-?\d+)\s*\).*/)
- if(shouldRegauge) {
- const [now, target] = [Number(shouldRegauge[1]), Number(shouldRegauge[2])]
- window.active_gage(now, target)
- if(now >= 0) {
- resetAction = false
- }
- }
- })
- if(resetAction) {
- doAction = true
- }
- }
- const updateRaw = (rawRoot, queryDocument) => {
- const frag = new DocumentFragment()
- const newBtn = document.createElement("button")
- const newBtnLbl = document.createTextNode("[New]")
- newBtn.append(newBtnLbl)
- newBtn.onclick = (e) => { rawVisible = !rawVisible; restyle(); }
- frag.append(newBtn)
- let copiedNodes = []
- for(let n of queryDocument.children) {
- copiedNodes.push(document.importNode(n, true))
- }
- copiedNodes.forEach(n => frag.append(n))
- rawRoot.replaceChildren(frag)
- }
- const doRawRequest = async (payload) => {
- if(latestRequest) {
- const timeNow = new Date().getTime()
- const timeOld = latestRequest
- latestRequest = Math.max(new Date().getTime(), latestRequest) + SMALLEST_INTERVAL + SAFETY_MARGIN
- await new Promise(r => setTimeout(r, Math.max(0, (SMALLEST_INTERVAL + SAFETY_MARGIN) - (timeNow - timeOld))))
- latestRequest = new Date().getTime()
- }
- const rawResp = await fetch("party.cgi",
- {
- method: "POST",
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded'
- },
- body: new URLSearchParams(payload).toString()
- })
- const respText = await rawResp.text()
- const parser = new DOMParser()
- const resp = parser.parseFromString(respText, "text/html")
- const raw = document.querySelector(".raw")
- if(raw) {
- updateRaw(raw, resp.body)
- }
- latestRequest = new Date().getTime()
- return resp
- }
- const doRequest = async (comment = "") => {
- const { id, pass } = JSON.parse(localStorage.getItem("heypartyCreds"))
- const fd = {}
- let brace = false
- if(comment.match(/@Proceed(\s+|$)/)) { brace = true } // hack the game does when proceeding... required to not receive the pre-proceed update.
- fd['id'] = id
- fd['pass'] = pass
- fd['reload_time'] = "0"
- fd['comment'] = comment
- let resp = await doRawRequest(fd)
- if(brace) {
- fd['comment'] = ""
- resp = await doRawRequest(fd)
- }
- return resp
- }
- const parseLogsChats = (messages) => {
- let chatMsgs = []
- let logMsgs = []
- messages.forEach(m => {
- const [speaker, text] = m.textContent.split(/:/, 2) // WARNING: THIS IS NOT IN FACT A COLON
- let words = text.split(/\s+/).filter(w => w.length > 0)
- const timestamp = words.slice(-2)
- words = words.slice(0, -2)
- const isOnlyCommands = speaker.match(/^@System$/) || m.querySelector("span") || words.every(w => w[0] === "@" || w[0] === ">")
- if(isOnlyCommands) {
- logMsgs.push(m)
- return;
- }
- const isOnlySpeech = !words.some(w => w.match(/(@.+)/))
- if(isOnlySpeech) {
- chatMsgs.push(m)
- return;
- }
- chatMsgs.push(m)
- logMsgs.push(m)
- })
- return [logMsgs, chatMsgs]
- }
- const updateStateDisplays = () => {
- const notice = document.querySelector(".pending-action")
- const commentNotice = document.querySelector(".pending-comment")
- if(!notice || !commentNotice) { return; }
- if(!queuedAction) {
- notice.innerHTML = ""
- notice.style.display = "none"
- } else {
- notice.innerHTML = "Pending: " + queuedAction.action
- notice.style.display = ""
- }
- if(queuedComments.length > 0) {
- commentNotice.innerHTML = `Comments:\n${queuedComments.join("\n")}`
- commentNotice.style.display = ""
- } else {
- commentNotice.innerHTML = ""
- commentNotice.style.display = "none"
- }
- }
- const queueAction = (action) => {
- queuedAction = action
- updateStateDisplays()
- }
- const queueComment = (comment) => {
- queuedComments.push(comment)
- updateStateDisplays()
- }
- const createMenu = (from) => {
- const frag = new DocumentFragment()
- frag.replaceChildren(document.importNode(from, true))
- return frag
- }
- const createAltMenu = (froms) => {
- const frag = new DocumentFragment()
- froms.forEach(from => {
- const node = document.importNode(from, true)
- const actionString = node.onclick.toString().match(/text_set\((.*?)\)/)[1].slice(1, -2)
- node.className = "alt-action"
- node.removeAttribute("onclick")
- node.onclick = async e => { queueAction({ action: actionString, type: 'action' }) }
- frag.append(node)
- })
- return frag
- }
- const requestCombat = async (tbl) => {
- const fd = {}
- if(!tbl) { return }
- Array.from(tbl.querySelectorAll("input,select")).forEach(e => {
- if(e.name && e.name.length > 0) {
- fd[e.name] = e.value
- }
- })
- const { id, pass } = JSON.parse(localStorage.getItem("heypartyCreds")) ?? { id: "", pass: "" }
- fd['id'] = id
- fd['pass'] = pass
- fd['comment'] = tbl.querySelector('input[type="submit"]').value
- await doRawRequest(fd) // combat create is special and always requires an update...
- return await doRequest()
- }
- const inferCombatState = (resp) => {
- const bgImg = resp.querySelector('div[style^="background"]')?.style.background?.match(/url\("(.*?)"\)/)
- const maybeInDungeon = bgImg? bgImg[1].match(/map\d+\..*/) : false
- const maybeInCombat = maybeInDungeon || (bgImg? bgImg[1].match(/(stage|challenge)\d+\..*/) : false) // Assume all maps have the form typeX and hardcode types...
- return [maybeInCombat? true : false, maybeInDungeon? true : false] // transform the match/null into true/false...
- }
- const updateElement = (queryDocument, elt, div) => {
- if(!div) { return }
- switch(elt) {
- case "status":
- const statusFrag = new DocumentFragment()
- const rawMes = queryDocument.querySelector(".mes:not(.status)")
- const [combatStatus, dungeonStatus] = inferCombatState(queryDocument)
- // setting directly by destructuring does not work and will instead replace
- // the value of rawMes...
- inCombat = combatStatus
- inDungeon = dungeonStatus
- const extraStatus = queryDocument.querySelector(".strong")
- const extraStatusIsMenu = extraStatus?.querySelector(".table1")
- const altExtraStatusIsMenu = extraStatus?.querySelector("span[onclick]")
- let copiedMes = []
- for(let n of rawMes.childNodes ?? []) {
- copiedMes.push(document.importNode(n, true))
- }
- copiedMes.forEach(n => statusFrag.append(n))
- if(extraStatus && !extraStatusIsMenu && !altExtraStatusIsMenu) {
- const thisStatus = document.importNode(extraStatus, true)
- statusFrag.append(thisStatus)
- lastExtraStatus = thisStatus
- } else if(!extraStatus && lastExtraStatus) {
- statusFrag.append(lastExtraStatus)
- lastExtraStatus = undefined
- }
- const notice = document.createElement("pre")
- notice.className = "pending-action"
- const commentNotice = document.createElement("pre")
- commentNotice.className = "pending-comment"
- const updateNotice = document.createElement("pre")
- updateNotice.className = "update-notice"
- const updateNoticeText = document.createTextNode("Next update: now")
- updateNotice.append(updateNoticeText)
- statusFrag.append(notice)
- statusFrag.append(commentNotice)
- statusFrag.append(updateNotice)
- div.replaceChildren(statusFrag)
- div.classList.add("mes")
- updateState(queryDocument)
- updateStateDisplays()
- break;
- case "room":
- const roomFrag = new DocumentFragment()
- const rawRoom = queryDocument.querySelector(".view") // party view
- if(rawRoom) {
- const roomView = document.importNode(rawRoom, true)
- div.append(roomView)
- const room = document.importNode(rawRoom.nextSibling, true)
- roomFrag.append(room) // actual room display
- } else {
- const combatRoom = queryDocument.querySelector(".mes").nextSibling
- const room = document.importNode(combatRoom, true)
- roomFrag.append(room) // actual room display
- }
- if(roomFrag.lastChild && roomFrag.lastChild.querySelector) {
- const maybeSelectable = roomFrag.lastChild.querySelector("table")
- if(maybeSelectable) {
- const selectables = maybeSelectable.querySelectorAll("td")
- selectables.forEach(s => {
- s.className = "selectable"
- const menu = document.createElement("div")
- menu.className = "selectable-menu"
- currentActions.concat("@Fap").forEach(a => {
- if((inCombat && !specialActions.some(act => act === a) && !ordinaryActions.some(act => act === a) && !untargetedCombatActions.some(act => act === a))
- || targetedActions.some(act => act === a) || a === "@Fap") {
- const menuItem = document.createElement("div")
- menuItem.className = "selectable-menu-item"
- const txt = document.createTextNode(a)
- menuItem.append(txt)
- menuItem.value = a + s.onclick.toString().match(/text_set\((.*?)\)/)[1].slice(1, -2)
- menuItem.removeAttribute("onclick")
- menuItem.onclick = (e) => { queueAction({ action: e.target.value, type: 'action' }) }
- menu.append(menuItem)
- }
- })
- s.removeAttribute("onclick")
- s.append(menu)
- })
- }
- }
- div.replaceChildren(roomFrag)
- break;
- case "chatbox":
- const chatboxFrag = new DocumentFragment()
- const chatLayout = document.createElement("div")
- chatLayout.className = "chat-layout"
- const chatField = document.createElement("input")
- chatField.type = "text"
- chatField.className = "ipt-chat"
- const chatSend = document.createElement("button")
- chatSend.className = "btn-send"
- chatSend.append(document.createTextNode("Send"))
- chatSend.onclick = (e) => {
- if(chatField.value.match(/@.+(\s+|$|>)/)) {
- queueAction({ action: chatField.value, type: 'literal' })
- } else {
- queueComment(chatField.value)
- }
- chatField.value = ""
- }
- chatField.addEventListener("keyup",
- (e) => {
- e.preventDefault();
- if (e.keyCode === 13) {
- chatSend.click()
- }
- })
- chatLayout.append(chatField)
- chatLayout.append(chatSend)
- chatboxFrag.append(chatLayout)
- div.replaceChildren(chatboxFrag)
- break;
- case "controls":
- const controlsFrag = new DocumentFragment()
- const actions = queryDocument.querySelectorAll(".actionLink")
- const actionsLayout = document.createElement("div")
- actionsLayout.className = 'actions-layout'
- let currentRow = 1
- currentActions = []
- actions.forEach(a => {
- if(["@Move", "@Attack", "@Use"].some(w => a.textContent === w)) {
- currentRow++
- }
- const btn = document.createElement("button")
- btn.onclick = (e) => { queueAction({ action: e.target.value, type: 'action' }) }
- btn.value = a.textContent
- const lbl = document.createTextNode(btn.value)
- currentActions.push(btn.value)
- btn.append(lbl)
- btn.className = 'btn-action'
- btn.style['grid-row-start'] = currentRow
- actionsLayout.append(btn)
- })
- currentRow++
- if(inCombat) {
- extraCombatActions.forEach(a => {
- const combatBtn = document.createElement("button")
- combatBtn.onclick = (e) => { queueAction({ action: a, type: 'action' }) }
- combatBtn.value = a
- const combatLbl = document.createTextNode(a)
- currentActions.push(a)
- combatBtn.append(combatLbl)
- combatBtn.className = 'btn-action'
- combatBtn.style['grid-row-start'] = currentRow
- actionsLayout.append(combatBtn)
- })
- currentRow++
- }
- if(inDungeon) {
- const glyphs = { "@North": "^", "@South": "v", "@East": ">", "@West": "<" }
- const positions = { "@North": [1, 2], "@South": [3, 2], "@East": [2, 3], "@West": [2, 1]}
- const dirLayout = document.createElement("div")
- dirLayout.className = "dir-container"
- Object.keys(glyphs).forEach(dir => {
- const btn = document.createElement("button")
- btn.className = "btn-dir"
- const btnLbl = document.createTextNode(glyphs[dir])
- btn.append(btnLbl)
- btn.onclick = e => { queueAction({ action: dir, type: 'action' }) }
- btn.style['grid-row-start'] = positions[dir][0]
- btn.style['grid-column-start'] = positions[dir][1]
- dirLayout.append(btn)
- })
- dirLayout.style['grid-row-start'] = currentRow
- actionsLayout.append(dirLayout)
- currentRow++
- }
- const rawBtn = document.createElement("button")
- const rawBtnLbl = document.createTextNode("[Original]")
- rawBtn.className = "btn-raw"
- rawBtn.append(rawBtnLbl)
- rawBtn.onclick = (e) => { rawVisible = !rawVisible; restyle(); }
- rawBtn.style['grid-row-start'] = currentRow
- actionsLayout.append(rawBtn)
- controlsFrag.append(actionsLayout)
- div.replaceChildren(controlsFrag)
- break;
- case "text-layout":
- const textFrag = new DocumentFragment()
- const [logs, chats] = parseLogsChats(queryDocument.querySelectorAll(".message"))
- const chatDiv = document.createElement("div")
- chatDiv.className = "chat"
- const chatHeader = document.createElement("div")
- chatHeader.className = "chat-header"
- const chatLbl = document.createTextNode("Chat")
- chatHeader.append(chatLbl)
- chatDiv.append(chatHeader)
- const logDiv = document.createElement("div")
- logDiv.className = "log"
- const logHeader = document.createElement("div")
- logHeader.className = "log-header"
- const logLbl = document.createTextNode("Log")
- logHeader.append(logLbl)
- logDiv.append(logHeader)
- textFrag.append(chatDiv)
- textFrag.append(logDiv)
- chats.forEach(cq => {
- const c = document.importNode(cq, true)
- const chatCard = document.createElement("div")
- chatCard.className = "message-card"
- chatCard.append(c)
- chatDiv.append(chatCard)
- })
- logs.forEach(cq => {
- const c = document.importNode(cq, true)
- const logCard = document.createElement("div")
- logCard.className = "log-card"
- logCard.append(c)
- logDiv.append(logCard)
- })
- div.replaceChildren(textFrag)
- break;
- case "menu":
- const extra = queryDocument.querySelector(".strong")
- const extraStatusMenu = extra?.querySelectorAll(".table1")
- const altExtraStatusMenu = extra?.querySelectorAll("span[onclick]")
- const invalid = extra?.textContent.includes("Go to") // Game does a hack with a never-seen intermediate page with this text in the menu section
- if((invalid || !altExtraStatusMenu) && (!extraStatusMenu || extraStatusMenu.length === 0)) {
- if(activeMenu) {
- div.replaceChildren(activeMenu)
- } else {
- div.replaceChildren(new DocumentFragment())
- }
- activeMenu = undefined
- return
- }
- const menuFrag = new DocumentFragment()
- if(extraStatusMenu && extraStatusMenu.length > 0) {
- if(extraStatusMenu.length > 1) {
- let menus = []
- const sel = document.createElement("select")
- menuFrag.append(sel)
- extraStatusMenu.forEach((e, i) => {
- const o = document.createElement("option")
- const submitMenu = e.querySelector('input[type="submit"]')?.value
- if(submitMenu) {
- o.value = submitMenu
- } else {
- o.value = `Menu ${i+1}`
- }
- const optionLbl = document.createTextNode(o.value)
- o.append(optionLbl)
- sel.append(o)
- const menuDiv = document.createElement("div")
- menuDiv.className = "menu-container"
- menuDiv.replaceChildren(createMenu(e))
- const partyData = JSON.parse(localStorage.getItem("heypartyParties")) ?? {}
- const thisPartyData = (partyData && partyData[submitMenu]) ?? Object.fromEntries([[submitMenu, {}]])
- partyData[submitMenu] = thisPartyData
- for(let [k, v] of Object.entries(thisPartyData)) {
- const maybeParty = menuDiv.querySelector(`.text_box1[name="${k}"],.select1[name="${k}"]`)
- if(maybeParty) {
- maybeParty.value = thisPartyData[k] ?? ""
- }
- }
- const menuSubmitButton = menuDiv.querySelector('input[type="submit"]')
- const partyField = menuDiv.querySelector('.text_box1[name="p_name"]')
- partyField.addEventListener("keyup",
- (e) => {
- e.preventDefault();
- if (e.keyCode === 13) {
- menuSubmitButton.click()
- }
- })
- if(partyField && menuSubmitButton) {
- menuSubmitButton.onclick = async (evt) => {
- evt.preventDefault()
- activeMenu = undefined // close the menu
- Array.from(menuDiv.querySelectorAll('.text_box1[name],.select1[name]')).forEach(e => partyData[submitMenu][e.name] = e.value)
- localStorage.setItem("heypartyParties", JSON.stringify(partyData))
- const resp = await requestCombat(menuDiv.querySelector(".table1"))
- const [maybeInCombat, maybeInDungeon] = inferCombatState(resp)
- if(resp && maybeInCombat) { // success
- await doRequest() // create update, i.e. enter the quest
- for(let elt of allElts) {
- updateElement(resp, elt, document.querySelector("." + elt))
- }
- } else {
- // failure, "error" status will need an update
- updateElement(resp, "status", document.querySelector(".status"))
- updateElement(resp, "menu", document.querySelector(".menu"))
- }
- }
- }
- menus[o.value] = menuDiv
- menuFrag.append(menuDiv)
- })
- sel.onchange = (e) => {
- Array.from(Object.entries(menus)).forEach(([k,div]) => div.style.display = "none")
- menus[e.target.value].style.display = ""
- }
- sel.selectedIndex = 0
- sel.dispatchEvent(new Event("change"))
- doAction = true
- } else if(extraStatusMenu) {
- console.log("One menu path", extraStatusMenu)
- const menuDiv = document.createElement("div")
- menuDiv.className = "menu-container"
- menuDiv.replaceChildren(createMenu(extraStatusMenu[0]))
- menuFrag.append(menuDiv)
- }
- activeMenu = menuFrag
- div.replaceChildren(menuFrag)
- doAction = true
- } else if(altExtraStatusMenu) {
- const menuDiv = document.createElement("div")
- menuDiv.className = "menu-container strong"
- const altMenu = createAltMenu(altExtraStatusMenu)
- menuDiv.append(document.importNode(extra.childNodes[0])) // text caption for the action
- menuDiv.append(document.createElement("br"))
- const actualMenuDiv = document.createElement("div")
- actualMenuDiv.className = "alt-menu"
- actualMenuDiv.append(altMenu)
- menuDiv.append(actualMenuDiv)
- menuFrag.append(menuDiv)
- activeMenu = menuFrag
- div.replaceChildren(menuFrag)
- doAction = true
- }
- break;
- }
- }
- let updateElements = (resp, which) => {
- for(let elt of (which ?? allElts)) {
- updateElement(resp, elt, document.querySelector("." + elt))
- }
- }
- const handleAction = async () => {
- if(!queuedAction) { return }
- const { action, type } = queuedAction
- lastExtraStatus = undefined
- activeMenu = undefined
- let trueAction = action.match(/.*?(@.+?)(\s+|$|>)/)
- let actionIsTargeted = action.match(/.*?(>.+?)(\s+|$|@)/)
- queuedAction = undefined
- if(trueAction) {
- trueAction = trueAction[1]
- }
- if(actionIsTargeted) {
- if(specialActionsWhenTargeted.some(a => a === trueAction)) {
- let unhandled = false;
- switch(trueAction) {
- case "@Move":
- case "@Party":
- case "@Dungeon":
- case "@GuildBattle":
- case "@Challenge":
- case "@Arena":
- await doRequest(action)
- const resp = await doRequest() // update page after the action that requires double-updates
- activeMenu = undefined
- updateElements(resp)
- break;
- default:
- unhandled = true;
- }
- if(!unhandled) {
- return
- }
- }
- }
- if(specialActions.some(a => a === trueAction)) {
- let unhandled = false;
- let resp = undefined;
- switch(trueAction) {
- // These actions will disable updates and show the raw page (with a special button to cancel)
- case "@Invite":
- case "@MonsterBook":
- case "@Profile":
- case "@ItemEncyclopedia":
- case "@JobMastery":
- await doRequest(action)
- const specResp = await doRequest()
- const oldBody = new DocumentFragment()
- Array.from(document.querySelector(".raw").children).forEach(c => oldBody.append(c))
- document.querySelector(".raw").replaceChildren(document.importNode(specResp.body, true))
- const backButton = document.createElement("button")
- backButton.className = "btn-back-from-special"
- const backButtonLbl = document.createTextNode("[Back]")
- backButton.append(backButtonLbl)
- backButton.onclick = e => { raw.replaceChildren(oldBody); rawVisible = false; restyle(); updatesEnabled = true }
- const raw = document.querySelector(".raw")
- raw.querySelector("input[value='Return']")?.remove() // remove the 'return' button that goes back to the raw page
- raw.insertBefore(backButton, raw.firstChild)
- updatesEnabled = false
- rawVisible = true
- restyle()
- break;
- // These actions function normally, but require double updates to properly progress
- // This is either to skip the "useless" "@X -> you did X! [Next]" screen,
- // or for those that have 'transitory states' that are never actually seen in the original UI
- // because they automatically generate a 2nd update...
- case "@Home":
- case "@RunAway":
- case "@Sleep":
- case "@Proceed":
- case "@Guild":
- case "@North":
- case "@South":
- case "@East":
- case "@West":
- await doRequest(action)
- resp = await doRequest() // update page after the action that requires double-updates
- updateElements(resp)
- break;
- case "@Logout":
- window.location.href = '/party2/index.cgi'
- break;
- // These actions simply disable updates until canceled.
- case "@ReadLetter":
- updatesEnabled = false
- resp = await doRequest(action)
- updateElements(resp)
- const endButton = document.createElement("button")
- endButton.className = "btn-back-from-special"
- const endButtonLbl = document.createTextNode("[Back]")
- endButton.append(endButtonLbl)
- endButton.onclick = async (e) => { updatesEnabled = true; const resp = await doRequest(); updateElements(resp); e.target.remove() }
- const statusMenu = document.querySelector(".menu-container.strong")
- statusMenu.lastElementChild.after(endButton)
- break;
- default:
- unhandled = true
- break;
- }
- if(!unhandled) {
- return
- }
- }
- // Assume all non-listed moves are skills while we are in combat...
- if((inCombat && !unhandledCombatActions.some(a => a === trueAction)) || (!inCombat && ordinaryActions.some(a => a === trueAction))) {
- let resp = await doRequest(action)
- if(inDungeon) {
- // Killing an enemy immediately moves to the map view without
- // requiring a 'proceed', so we have to account for that in dungeons
- resp = await doRequest()
- }
- updateElements(resp)
- return
- }
- if(type === "literal") {
- const resp = await doRequest(action)
- updateElements(resp)
- return
- }
- const chatBox = document.querySelector(".ipt-chat")
- if(chatBox) {
- chatBox.value = action
- chatBox.focus()
- }
- }
- const tickFunction = async () => {
- const root = document.querySelector(".heyparty")
- if(ticking || !root) { return; } // waiting for override() to get called
- ticking = true
- let doUpdate = false
- const currTime = new Date().getTime()
- if(prevTime) {
- if(currTime - prevTime < TICK_INTERVAL) {
- ticking = false
- return
- } else if(currTime - (prevUpdateTime ?? 0) >= UPDATE_INTERVAL + SAFETY_MARGIN) {
- if(!sleepTimer) { // don't do updates when we're asleep
- doUpdate = true
- }
- }
- }
- if(doAction && gaugeDone && queuedAction) {
- await new Promise(r => setTimeout(r, SAFETY_MARGIN))
- await handleAction()
- } else if(queuedComments.length > 0 && (!prevCommentTime || (currTime - prevCommentTime >= COMMENT_INTERVAL + SAFETY_MARGIN))) {
- const comment = queuedComments.shift()
- const resp = await doRequest(comment)
- updateElements(resp, ["status", "text-layout", "room"])
- prevCommentTime = new Date().getTime()
- } else if(doUpdate && updatesEnabled) {
- const resp = await doRequest()
- updateElements(resp, ["text-layout", "room", "status"])
- prevUpdateTime = new Date().getTime()
- }
- prevTime = new Date().getTime()
- ticking = false
- }
- const override = async () => {
- const myRoot = document.createElement("div")
- myRoot.className = "heyparty"
- const hey = document.createElement("div")
- hey.className = "hey"
- // STYLE STYLE STYLE
- const myRootStyle = document.createElement("style")
- const styleContents = document.createTextNode(`
- .hey {
- }
- .raw {
- }
- .dir-container {
- display: grid;
- grid-template-columns: 1fr 1fr 1fr;
- grid-template-rows: 1fr 1fr 1fr;
- max-width: 128px;
- max-height: 128px;
- }
- .chat-layout {
- display: flex;
- flex-direction: row;
- }
- .actions-layout {
- display: grid;
- grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
- grid-auto-flow: row;
- row-gap: 1ch;
- max-width: 256px;
- justify-items: start;
- overflow: visible;
- }
- .chat {
- display: inline-block;
- padding-left: 10px;
- padding-right: 10px;
- width: 40%;
- }
- .log {
- display: inline-block;
- padding-left: 10px;
- padding-right: 10px;
- width: 40%;
- }
- .text-layout {
- display: flex;
- flex-direction: row;
- align-items: start;
- }
- .selectable:not(:hover) .selectable-menu {
- display: none;
- }
- .selectable-menu {
- position: absolute;
- display: inline;
- z-index: 1;
- background: #336;
- border: 1px white solid;
- }
- .selectable:hover .selectable-menu {
- display: inline;
- }
- .selectable-menu:hover {
- display: inline;
- }
- .selectable-menu-item:hover {
- background: #666;
- cursor: pointer;
- }
- .menu-container {
- height: 256px;
- overflow: auto;
- }
- .btn-action {
- border: none;
- background: none;
- color: white;
- cursor: pointer;
- font-weight: bold;
- font-size: 1em;
- }
- .chat-header {
- margin-top: 0.5em;
- margin-bottom: 0.5em;
- font-size: 1.5rem;
- font-weight: bold;
- color: white;
- border-style: dashed none dashed none;
- }
- .log-header {
- margin-top: 0.5em;
- margin-bottom: 0.5em;
- font-size: 1.5rem;
- font-weight: bold;
- color: white;
- border-style: dashed none dashed none;
- }
- .alt-menu {
- display: flex;
- flex-direction: row;
- justify-content: start;
- text-align: left;
- align-items: flex-start;
- flex-wrap: wrap;
- max-width: 512px;
- }
- .alt-action {
- margin-left: 3px;
- margin-right: 3px;
- margin-top: 3px;
- margin-bottom: 3px;
- cursor: pointer;
- }
- `)
- myRootStyle.append(styleContents)
- let allEltsDiv = []
- for(let elt of allElts) {
- const div = document.createElement("div")
- div.className = elt
- allEltsDiv.push(div)
- }
- for(let idx in allElts) {
- updateElement(document, allElts[idx], allEltsDiv[idx])
- }
- allEltsDiv.forEach(div => hey.append(div))
- const rawRoot = document.createElement("div")
- rawRoot.className = "raw"
- rawRoot.style.display = "none"
- myRoot.append(hey)
- myRoot.append(rawRoot)
- const myBody = document.createElement("body")
- myBody.append(myRoot)
- const form = document.querySelector("#form")
- const id = form.id.value
- const pass = form.pass.value
- localStorage.setItem("heypartyCreds", JSON.stringify({ id, pass }))
- updateRaw(rawRoot, document.body)
- document.body = myBody
- document.head.append(myRootStyle)
- window.count_down = (nowTs) => {
- origCD(nowTs)
- }
- window.wake_time = (nowTs) => { }
- window.active_gage = (nowTs, targetTs) => {
- const up = document.querySelector(".update-notice")
- if(nowTs > 0) {
- gaugeEngaged = true
- up.innerHTML = ("Next update: " + (nowTs) + "s")
- gaugeDone = false
- doAction = false
- }
- if(nowTs === 0) {
- gaugeEngaged = false
- up.innerHTML = "Next update: now"
- gaugeDone = true
- doAction = true
- }
- origGage(nowTs, targetTs)
- }
- if(tickInterval) {
- clearInterval(tickInterval)
- tickInterval = undefined
- }
- if(!tickInterval) {
- tickInterval = setInterval(tickFunction, TICK_INTERVAL)
- }
- }
- if(!document.querySelector(".heyparty")) {
- await override()
- }
- }
- retryTimeout = setTimeout(main, SMALLEST_INTERVAL + SAFETY_MARGIN)
Advertisement
Add Comment
Please, Sign In to add comment