// ==UserScript==
// @name HeyParty
// @namespace Violentmonkey Scripts
// @match *://cgi.heyuri.net/party2/*
// @grant none
// @version 1.2.3
// @author SordFish
// @license MIT
// @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 COMBINED_CHATLOG = false // True to combine the logs and the chats
const PENDING_STATUS = false // False to disable pending status messages in the status area (can be wobbly and distracting)
const AGGRESSIVE_LOG_FILTER = true // true: any message with @ + capital letter, or @ + $, is considered a log message. False: mixed command+comment are also echo'd to the chat area (includes false negatives).
const MACROS_DISABLED = false // Whether to show the kaomoji/chat macro buttons
const AUTOFOCUS = true // Whether to autofocus on the chatbox when doing most actions
const SHOW_SECRETS = true // Whether to show secret actions
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 macrosVisible = true
let macrosEdit = true // XXX
let rawVisible = localStorage.getItem("heypartyShowOrig") ?? false
let queuedAction = undefined
let queuedComments = []
let pendingRef = undefined
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 performingAction = false
let inCombat = false
let inDungeon = false
let menuVisible = false
let activeMenu = undefined
let kaomoji =
[
"ヽ(´ー`)ノ",
"(;´Д`)",
"ヽ(´∇`)ノ",
"(´人`)",
"(^Д^)",
"(´ー`)",
"( ´,_ゝ`)",
"(゚ー`)",
"( ´ω`)",
"ヽ(`Д´)ノ",
"┐(゚~゚)┌",
"(;゚∀゚)",
"(;゚Д゚)",
"(´~`)",
"(・∀・)",
"Σ(;゚Д゚)",
"Σ(゚д゚|||)",
"(⌒∇⌒ゞ)",
"(゚血゚#)",
"キタ━━━(・∀・)━━━!!",
"(゚ー゚)",
"(´¬`)",
"(´π`)",
"ヽ(゚ρ゚)ノ",
]
let savedMacros = JSON.parse(localStorage.getItem("heypartyMacros"))
let chatMacros = savedMacros
if(savedMacros === null) {
chatMacros = kaomoji
localStorage.setItem("heypartyMacros", JSON.stringify(chatMacros))
}
// Update order.
// Note that order does matter: 'room' generates the hover menu based on 'controls', for example.
const allElts = ["status", "chatbox", "controls", "room", "menu", "text-layout"]
// Elements that should be updated on state change. E.g. chatbox only needs to be updated on init, never again
const allUpdateableElts = ["status", "controls", "room", "menu", "text-layout"]
// Display order, for override.
const displayElts = ["status", "room", "chatbox", "controls", "menu", "text-layout"]
// Actions default to manual operations, i.e. just send the action to the chatbox and focus it...
let secretActions = [
"@Fap",
"@Slap",
]
// 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",
"@BlackMarket",
"@SecretShop",
"@Basement"
]
// Actions that should instantly dispatch
let ordinaryActions = [
"@PuffPuff",
"@Town",
"@Send",
"@GiveName",
"@Guild",
"@Move",
"@Make",
"@Spectate",
"@Offer",
"@Trade",
"@Entry",
"@View",
"@Wallpaper",
"@Exchange",
"@Color",
"@ChangeJob",
"@Order",
"@Withdraw",
"@Deposit",
"@Sort",
"@Use",
"@Sell",
"@Buy",
"@Transfer",
"@Release",
"@PrizeExchange",
"@$1slot",
"@$10slot",
"@$100slot",
"@Look",
"@Resolve",
"@Tombola",
"@Prizes",
"@Alchemy",
"@Recipe",
"@Examine",
"@Speak",
"@Map",
"@Fap",
"@Slap",
]
// 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",
"@Spectate",
"@Join",
"@Town",
"@Send",
"@GiveName"
]
// Actions to always show in combat
let extraCombatActions = [
//"@Proceed",
//^ Not needed anymore since we're doing double-updates...
]
// 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",
"@Whisper",
"@Slap",
]
let untargetedCombatActions = [
"@West",
"@East",
"@North",
"@South",
"@Map",
"@Start",
"@Party",
"@Defend",
"@Tension",
"@Rear",
"@Front",
"@Screenshot"
]
// These actions will fill the chatbox instead of being performed
// Mostly just for arithmetician...
let unhandledCombatActions = [
"@MP5",
"@MP4",
"@MP3",
"@Screenshot",
"@Whisper"
]
let currentActions = []
const focusChat = (force = false) => {
if(force || (AUTOFOCUS /*&& !macrosEdit*/)) { // XXX
const chat = document.querySelector(".ipt-chat")
if(chat) {
chat.focus()
chat.selectionStart = chat.selectionEnd = chat.value.length
}
}
}
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 setRawVisible = (state) => {
updatesEnabled = !state
localStorage.setItem("heypartyShowOrig", state)
rawVisible = state
restyle()
}
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) => { setRawVisible(false); focusChat() }
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)
let isOnlyCommands = false
if(AGGRESSIVE_LOG_FILTER) {
isOnlyCommands = speaker.match(/^@System$/) || m.querySelector("span,img") || m.textContent.match(/@([A-Z]|\$)[^\s]*/)
} else {
isOnlyCommands = speaker.match(/^@System$/) || m.querySelector("span,img")
|| (speaker[0] === '@' && (words.length > 0 && words[0][0] === '@'))
|| words.every(w => w[0] === "@" || w[0] === ">")
|| m.textContent.match(/[^\s]+\s*:\s*@[^\s]+\s+missed!\s+[^\s]+\s+dodged it!/)
|| m.textContent.match(/\s*[^\s]+\s*:\s*@Sleep(>[^\s]+)?\s+[^\s]+\s+crawled into bed!/)
|| m.textContent.match(/\s*[^\s]+\s*:\s*@Item(>[^\s]+)?\s+[^\s]+\s+used\s+[^\s]+?!/)
|| m.textContent.match(/@[^\s]+.*? However,\s+[^\s]+?\s+cannot move!/)
|| m.textContent.match(/@[^\s]+.*? is sleeping!/)
|| m.textContent.match(/@[^\s]+.*? woke up from their sleep!/)
|| m.textContent.match(/@[^\s]+.*? is paralyzed and unable to move!/)
|| m.textContent.match(/@[^\s]+.*It didn't seem to have any more effect on\s\+[^\s]+?\.\.\./)
}
if(isOnlyCommands) {
logMsgs.push(m)
return;
}
const isOnlySpeech = !words.some(w => w.match(/@([A-Z]|\$)[^\s]*/))
if(isOnlySpeech) {
chatMsgs.push(m)
return;
}
chatMsgs.push(m)
logMsgs.push(m)
})
return [logMsgs, chatMsgs]
}
const updateStateDisplays = () => {
if(!PENDING_STATUS) { return; }
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) => {
focusChat()
queuedAction = action
updateStateDisplays()
}
const queueComment = (comment) => {
focusChat()
queuedComments.push(comment)
updateStateDisplays()
}
const labelPending = (target) => {
if((pendingRef?.value ?? "%>") === (target?.value ?? "%<")) {
target.classList.add("pending")
}
}
const resetPending = () => {
pendingRef = undefined
Array.from(document.querySelectorAll('.pending')).forEach(p => p.classList.remove('pending'))
}
const togglePending = (target) => {
if(!performingAction) {
Array.from(document.querySelectorAll('.pending')).forEach(p => p.classList.remove('pending'))
if(pendingRef != target) {
target.classList.add("pending")
pendingRef = target
} else {
pendingRef = undefined
}
}
}
const createMenu = (from) => {
const frag = new DocumentFragment()
const node = document.importNode(from, true)
Array.from(node.querySelectorAll('tr[onclick]')).forEach(r => {
let action = r.onclick.toString().match(/text_set\('(.*?)'\)/)
action = action ? action[1] : ""
r.removeAttribute("onclick")
r.onclick = e => { queueAction({ action: action, type: 'action' }); togglePending(e.target) }
r.value = action
labelPending(r)
r.style.cursor = "pointer"
})
frag.replaceChildren(node)
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, -1)
node.className = "alt-action"
node.removeAttribute("onclick")
node.onclick = e => { queueAction({ action: actionString, type: 'action' }); togglePending(e.target); activeMenu = undefined; }
node.value = actionString
labelPending(node)
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 atGuild = resp.querySelector(".mes img[alt^='Guild emblem']")
const atHome = resp.querySelector(".mes").textContent.match(/【[^\s]+'s house】/)
const maybeInDungeon = (!atHome && !atGuild && bgImg)? bgImg[1].match(/map\d+\..*/) : false
const maybeInCombat = maybeInDungeon || ((!atHome && !atGuild && 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 toggleEdit = () => {
if(macrosEdit) {
//macrosEdit = false
document.querySelector(".btn-macros-edit-toggle").textContent = "Start Editing"
//Array.from(document.querySelectorAll(".macros-edit-container")).forEach(k => k.style.display = "none")
//document.querySelector(".btn-macros-add").style.display = "none"
} else {
macrosEdit = true
document.querySelector(".btn-macros-edit-toggle").textContent = "Stop Editing"
//document.querySelector(".btn-macros-add").style.display = "inherit"
Array.from(document.querySelectorAll(".macros-edit-container")).forEach(k => k.style.display = "")
}
}
const toggleMacros = () => {
if(macrosVisible) {
macrosVisible = false
macrosEdit = true
toggleEdit() // force to off with full hiding/handling
Array.from(document.querySelectorAll(".btn-macros")).forEach(k => k.classList.add("macros-hidden"))
//document.querySelector(".btn-macros-edit-toggle").style.display = "none"
document.querySelector(".btn-macros-toggle").textContent = "Show Macros"
document.querySelector(".btn-macros-add").style.display = "none"
} else {
macrosVisible = true
macrosEdit = true
toggleEdit()
Array.from(document.querySelectorAll(".btn-macros")).forEach(k => k.classList.remove("macros-hidden"))
document.querySelector(".btn-macros-toggle").textContent = "Hide Macros"
//document.querySelector(".btn-macros-edit-toggle").style.display = "inherit"
document.querySelector(".btn-macros-edit-toggle").textContent = "Start Editing"
document.querySelector(".btn-macros-add").style.display = ""
document.querySelector(".btn-macros-toggle").textContent = "Hide Macros"
}
}
const createMacroButton = (k, i) => {
const macroButton = document.createElement("button")
macroButton.value = k
macroButton.className = `btn-macros ${!macrosVisible? "macros-hidden" : ""}`
const macroPre = document.createElement("pre")
const macroLbl = document.createTextNode(k)
macroPre.append(macroLbl)
macroButton.append(macroPre)
const editBtn = document.createElement("button")
const delBtn = document.createElement("button")
const delLbl = document.createTextNode("X")
const editLbl = document.createTextNode("O")
editBtn.append(editLbl)
delBtn.append(delLbl)
const macrosEditContainer = document.createElement("div")
macrosEditContainer.append(editBtn)
macrosEditContainer.append(delBtn)
macrosEditContainer.className = "macros-edit-container"
macrosEditContainer.style.display = macrosEdit? "inherit" : "none"
editBtn.className = "btn-macros-edit"
delBtn.className = "btn-macros-delete"
const editForm = document.createElement("div")
editForm.className = "macros-edit-form"
const editField = document.createElement("input")
editField.className = "macros-edit-field"
editField.value = k
const editOK = document.createElement("button")
editOK.className = "macros-edit-ok"
const editCancel = document.createElement("button")
editCancel.className = "macros-edit-cancel"
const okLbl = document.createTextNode("O")
const cancelLbl = document.createTextNode("X")
editCancel.append(cancelLbl)
editOK.append(okLbl)
editForm.style.display = "none"
editForm.append(editField)
editForm.append(editOK)
editForm.append(editCancel)
const macrosEditControls = document.createElement("div")
macrosEditControls.append(macrosEditContainer)
macrosEditControls.append(editForm)
macrosEditControls.className = "macros-edit-controls"
macroButton.append(macrosEditControls)
editField.addEventListener("keypress",
(e) => {
if (e.key === "Enter") {
editOK.click()
}
})
macroButton.addEventListener("keypress", e => { if(e.key === "Enter") { e.preventDefault(); } })
editCancel.onclick = e => {
macroButton.querySelector(".macros-edit-form").style.display = "none"
macroButton.querySelector(".macros-edit-field").value = ""
}
editOK.onclick = e => {
macroButton.querySelector(".macros-edit-form").style.display = "none"
const edit = macroButton.querySelector(".macros-edit-field")
if(edit?.value === "") {
// pretend it's a cancel by default instead of deleting it...
} else {
chatMacros[i] = edit.value
macroButton.querySelector("pre").textContent = edit.value
macroButton.value = edit.value
}
localStorage.setItem("heypartyMacros", JSON.stringify(chatMacros))
}
delBtn.onclick = e => {
chatMacros = chatMacros.filter(x => x !== k)
localStorage.setItem("heypartyMacros", JSON.stringify(chatMacros))
macroButton.remove()
}
editBtn.onclick = e => {
macroButton.querySelector(".macros-edit-form").style.display = ""
editField.focus()
}
macroButton.onclick = e => { if(e.target == macroButton || e.target == macroPre) { const chatField = document.querySelector(".ipt-chat"); chatField.value += macroButton.value; focusChat() } }
macroButton.style.cursor = "pointer"
return macroButton
}
const updateElement = (queryDocument, elt, div) => {
if(!queryDocument.querySelector(".mes").nextSibling) { return }
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:not(.menu-container)")
const extraStatusIsMenu = extraStatus?.querySelector(".table1,.view")
const altExtraStatusIsMenu = extraStatus?.querySelector(":not(.view) 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("div")
updateNotice.className = "update-notice-container"
const updateNoticePre = document.createElement("pre")
updateNoticePre.className = "update-notice"
const updateNoticeText = document.createTextNode("Next update: now")
updateNoticePre.append(updateNoticeText)
updateNotice.append(updateNoticePre)
// The following two elements are required for the hijacked original functions to run...
updateNotice.append(document.importNode(queryDocument.querySelector("#gage_back1")))
const nokoriDiv = document.createElement("div")
nokoriDiv.style.display = "none"
nokoriDiv.id = "nokori_auto_time"
updateNotice.append(nokoriDiv)
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()
let rawRoom = queryDocument.querySelector(".view") // party view OR screenshot view!!
let isScreenshot = undefined
if(queryDocument.querySelector(".strong .view")) {
isScreenshot = document.importNode(rawRoom, true)
rawRoom = undefined
// It's actually the screenshot room
}
if(rawRoom) {
const roomView = document.importNode(rawRoom, true)
Array.from(roomView.querySelectorAll("span[onclick]")).forEach(span => {
const maybeActionType = span.onclick.toString().match(/text_set\('(.*?)'\)/)
const actionType = maybeActionType? maybeActionType[1] : ""
span.removeAttribute("onclick")
span.style.cursor = "pointer"
span.onclick = e => { queueAction({ action: actionType, type: 'action' }); pendingRef = e.target }
})
roomFrag.append(roomView)
const room = document.importNode(rawRoom.nextSibling, true)
room.classList.add("room")
roomFrag.append(room) // actual room display
} else {
const combatRoom = queryDocument.querySelector(".mes").nextSibling
const room = document.importNode(combatRoom, true)
room.classList.add("room")
roomFrag.append(room) // actual room display
}
const createMenuItem = (s, a) => {
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' })
togglePending(e.target)
}
labelPending(menuItem)
return menuItem
}
if(roomFrag.lastChild && roomFrag.lastChild.querySelector && !isScreenshot) {
const maybeSelectable = roomFrag.lastChild.querySelector("table")
if(maybeSelectable) {
const selectables = maybeSelectable.querySelectorAll("td[onclick]")
selectables.forEach(s => {
s.className = "selectable"
const menu = document.createElement("div")
menu.className = "selectable-menu"
menu.style.display = "none"
currentActions.forEach(a => {
if((inCombat && !specialActions.some(act => act === a) && !ordinaryActions.some(act => act === a) && !untargetedCombatActions.some(act => act === a))
|| targetedActions.some(act => act === a)) {
const menuItem = createMenuItem(s, a)
menu.append(menuItem)
}
})
menu.onclick = e => {
if(menuVisible) {
e.stopPropagation()
menuVisible = false
menu.style.display = "none"
}
}
/*if(SHOW_SECRETS) {
secretActions.forEach(a => {
if(!inCombat) {
const menuItem = createMenuItem(s, a)
menu.append(menuItem)
}
})
}*/
s.removeAttribute("onclick")
s.style.cursor = "pointer"
s.onclick = e => {
if(!menuVisible) {
e.stopPropagation()
menuVisible = s.querySelector("img[alt]").alt
menu.style.display = ""
}
}
s.append(menu)
})
}
}
let newVisible = false
Array.from(roomFrag.querySelectorAll(".selectable")).forEach(s => {
if(s.querySelector("img[alt]").alt === menuVisible) {
s.querySelector(".selectable-menu").style.display = ""
newVisible = menuVisible
}
})
menuVisible = newVisible
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]+(\s+|$|>)/)) {
queueAction({ action: chatField.value, type: 'literal' })
pendingRef = chatField
} else {
queueComment(chatField.value)
}
chatField.value = ""
}
chatField.addEventListener("keypress",
(e) => {
if (e.key === "Enter") {
chatSend.click()
}
})
const macrosLayout = new DocumentFragment()
if(!MACROS_DISABLED) {
const macrosToggle = document.createElement("button")
macrosToggle.onclick = e => { toggleMacros(); focusChat() }
const toggleLbl = document.createTextNode(`${macrosVisible? "Hide ": "Show "} Macros`)
macrosToggle.append(toggleLbl)
macrosToggle.className = "btn-macros-toggle"
const editToggle = document.createElement("button")
editToggle.onclick = e => { toggleEdit(); focusChat() }
const editLbl = document.createTextNode(`${macrosEdit? "Stop ": "Start "} Editing`)
editToggle.append(editLbl)
editToggle.className = "btn-macros-edit-toggle"
//XXX
editToggle.style.display = "none"
const newBtn = document.createElement("button")
//newBtn.style.display = macrosEdit? "" : "none"
newBtn.onclick = e => {
chatMacros.push("...")
const btn = createMacroButton("", chatMacros.length-1)
btn.querySelector("pre").textContent = "..."
const cancelEditBtn = btn.querySelector(".macros-edit-cancel")
const prevOnclick = cancelEditBtn.onclick
cancelEditBtn.onclick = e => {
prevOnclick(e)
chatMacros = chatMacros.slice(0, -1)
btn.remove()
}
const okEditBtn = btn.querySelector(".macros-edit-ok")
const prevOK = okEditBtn.onclick
const edit = btn.querySelector(".macros-edit-field")
okEditBtn.onclick = e => {
cancelEditBtn.onclick = prevOnclick
okEditBtn.onclick = prevOK
if(edit?.value === "") {
chatMacros.splice(chatMacros.length-1, 1)
btn.remove()
} else {
prevOK(e)
}
}
document.querySelector(".macros-btn-container").append(btn)
btn.querySelector(".btn-macros-edit").click()
}
const newBtnLbl = document.createTextNode("Add")
newBtn.append(newBtnLbl)
newBtn.className = "btn-macros-add"
newBtn.style.display = macrosVisible? "" : "none" //XXX
const macrosBase = document.createElement("div")
macrosBase.className = "macros-container"
const btnContainer = document.createElement("div")
btnContainer.className = "macros-controls-container"
btnContainer.append(macrosToggle)
btnContainer.append(editToggle)
btnContainer.append(newBtn)
macrosBase.append(btnContainer)
const macrosBtnContainer = document.createElement("div")
macrosBtnContainer.className = "macros-btn-container"
chatMacros.forEach((k, i) => {
const macroButton = createMacroButton(k, i)
macrosBtnContainer.append(macroButton)
})
macrosBase.append(macrosBtnContainer)
macrosLayout.replaceChildren(macrosBase)
}
chatLayout.append(chatField)
chatLayout.append(chatSend)
chatLayout.append(macrosLayout)
chatboxFrag.append(chatLayout)
div.replaceChildren(chatboxFrag)
focusChat()
break;
case "controls":
const controlsFrag = new DocumentFragment()
const actions = queryDocument.querySelectorAll(".actionLink")
const actionsLayout = document.createElement("div")
actionsLayout.className = 'actions-layout'
let currentTrack = document.createElement("div")
currentTrack.className = "action-track"
actionsLayout.append(currentTrack)
currentActions = []
actions.forEach(a => {
if((a.previousSibling?.nodeName ?? "") === "BR") {
currentTrack = document.createElement("div")
currentTrack.className = "action-track"
actionsLayout.append(currentTrack)
}
const btn = document.createElement("button")
btn.onclick = (e) => { queueAction({ action: e.target.value, type: 'action' }); togglePending(e.target); }
btn.value = a.textContent
const lbl = document.createTextNode(btn.value)
currentActions.push(btn.value)
btn.append(lbl)
btn.className = 'btn-action'
labelPending(btn)
currentTrack.append(btn)
})
if(SHOW_SECRETS && !inCombat) {
currentTrack = document.createElement("div")
currentTrack.className = "action-track"
actionsLayout.append(currentTrack)
secretActions.forEach(a => {
const btn = document.createElement("button")
btn.onclick = (e) => { queueAction({ action: e.target.value, type: 'action' }); togglePending(e.target); }
btn.value = a
const lbl = document.createTextNode(btn.value)
currentActions.push(btn.value)
btn.append(lbl)
btn.className = 'btn-action'
labelPending(btn)
currentTrack.append(btn)
})
}
currentTrack = document.createElement("div")
currentTrack.className = "action-track"
actionsLayout.append(currentTrack)
if(inCombat) {
extraCombatActions.forEach(a => {
const combatBtn = document.createElement("button")
combatBtn.onclick = (e) => { queueAction({ action: a, type: 'action' }); togglePending(e.target); }
combatBtn.value = a
const combatLbl = document.createTextNode(a)
currentActions.push(a)
combatBtn.append(combatLbl)
combatBtn.className = 'btn-action'
labelPending(combatBtn)
currentTrack.append(combatBtn)
})
currentTrack = document.createElement("div")
currentTrack.className = "action-track"
actionsLayout.append(currentTrack)
}
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' }); pendingRef = e.target }
btn.style['grid-row-start'] = positions[dir][0]
btn.style['grid-column-start'] = positions[dir][1]
dirLayout.append(btn)
})
currentTrack.append(dirLayout)
currentTrack = document.createElement("div")
currentTrack.className = "action-track"
actionsLayout.append(currentTrack)
}
const rawBtn = document.createElement("button")
const rawBtnLbl = document.createTextNode("[Original]")
rawBtn.className = "btn-raw"
rawBtn.append(rawBtnLbl)
rawBtn.onclick = (e) => { setRawVisible(true); focusChat() }
currentTrack.append(rawBtn)
controlsFrag.append(actionsLayout)
div.replaceChildren(controlsFrag)
break;
case "text-layout":
const textFrag = new DocumentFragment()
let chatDiv = undefined
if(!COMBINED_CHATLOG) {
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)
textFrag.append(chatDiv)
}
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(logDiv)
if(!COMBINED_CHATLOG) {
const [logs, chats] = parseLogsChats(queryDocument.querySelectorAll(".message"))
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)
})
} else {
queryDocument.querySelectorAll(".message").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:not(.menu-container)")
const extraStatusMenu = extra?.querySelectorAll(".table1,.view")
const altExtraStatusMenu = extra?.querySelectorAll(":not(.view) 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
const menuFrag = new DocumentFragment()
if((invalid || (altExtraStatusMenu?.length ?? 0) === 0) && (!extraStatusMenu || extraStatusMenu.length === 0)) {
if(activeMenu) {
menuFrag.replaceChildren(activeMenu)
}
div.replaceChildren(menuFrag)
return
}
const bScreenshot = extra.querySelector(".view")
if(extraStatusMenu && extraStatusMenu.length > 0) {
if(!bScreenshot && 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"]')
if(partyField) {
partyField.addEventListener("keypress",
(e) => {
if (e.key === "Enter") {
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(!bScreenshot && extraStatusMenu) {
const menuDiv = document.createElement("div")
menuDiv.className = "menu-container"
menuDiv.replaceChildren(createMenu(extraStatusMenu[0]))
menuFrag.append(menuDiv)
} else if(bScreenshot) {
let menus = []
activeMenu = undefined
const sel = document.createElement("select")
menuFrag.append(sel)
const scView = extra.querySelector(".view")
Array.from(scView.querySelectorAll("span[onclick]")).forEach((e, i) => {
const o = document.createElement("option")
o.value = `Screenshot ${i+1}`
const optionLbl = document.createTextNode(o.value)
o.append(optionLbl)
sel.append(o)
const menuDiv = document.createElement("div")
menuDiv.className = "menu-container"
const screenshot = document.importNode(e, true)
const action = screenshot.onclick.toString().match(/text_set\('(.*?)'\)/)[1]
screenshot.removeAttribute('onclick')
screenshot.onclick = e => { const chat = document.querySelector(".ipt-chat"); if(chat) { chat.value = action; }; }
screenshot.style.cursor = "pointer";
menuDiv.replaceChildren(screenshot)
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"))
}
activeMenu = document.importNode(menuFrag, true)
div.replaceChildren(menuFrag)
doAction = true
} else if(!bScreenshot && (altExtraStatusMenu?.length ?? 0) !== 0) {
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 = document.importNode(menuFrag, true)
div.replaceChildren(menuFrag)
doAction = true
}
break;
}
}
let updateElements = (resp, which) => {
for(let elt of (which ?? allUpdateableElts)) {
updateElement(resp, elt, document.querySelector("." + elt))
}
}
const handleAction = async () => {
if(!queuedAction) { return }
let { action, type } = queuedAction
let disableOrdinaryHandling = false
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":
case "@Join":
case "@Spectate":
await doRequest(action)
const resp = await doRequest() // update page after the action that requires double-updates
activeMenu = undefined
updateElements(resp)
break;
case "@Send":
case "@GiveName":
disableOrdinaryHandling = true
unhandled = true
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); setRawVisible(false); focusChat() }
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)
setRawVisible(true)
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":
case "@BlackMarket":
case "@SecretShop":
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) && trueAction[1] !== 'x')) || (!inCombat && ordinaryActions.some(a => a === trueAction) && !disableOrdinaryHandling)) {
let resp = await doRequest(action)
if(inCombat) {
resp = await doRequest()
// Killing an enemy immediately moves to the "map view" (in @Dungeons) or the post-combat 'state'
// so we have to account for that in combat states
}
updateElements(resp)
return
}
// Special override for arithmetician skill part 2
if(inCombat && trueAction[1] == 'x') {
const textField = document.querySelector('.ipt-chat')
let resp = await doRequest(textField.value + action)
textField.value = ''
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
focusChat(true)
}
}
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 && pendingRef) {
await new Promise(r => setTimeout(r, 2*SAFETY_MARGIN))
performingAction = true
await handleAction()
resetPending()
focusChat()
prevUpdateTime = new Date().getTime()
performingAction = false
} 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", "controls", "room"])
prevCommentTime = new Date().getTime()
prevUpdateTime = new Date().getTime()
} else if(doUpdate && updatesEnabled) {
const resp = await doRequest()
updateElements(resp, ["text-layout", "controls", "room", "status"])
prevUpdateTime = new Date().getTime()
}
prevTime = new Date().getTime()
ticking = false
}
const override = async () => {
const myRoot = document.createElement("div")
myRoot.className = "heyparty"
myRoot.addEventListener('click', e => {
if(menuVisible) {
if(!myRoot.querySelector(".selectable-menu-item:hover")) {
e.stopPropagation()
Array.from(myRoot.querySelectorAll(".selectable-menu:not([style^='display: none'])")).forEach(m => m.style.display = 'none')
menuVisible = false
}
focusChat()
}
},
true) // true for capture
const hey = document.createElement("div")
hey.className = "hey"
// STYLE STYLE STYLE
const myRootStyle = document.createElement("style")
const styleContents = document.createTextNode(`
.hey {
}
.raw {
}
.macros-edit-controls {
position: absolute;
display: inline;
}
.macros-edit-form {
position: absolute;
display: flex;
flex-direction: row;
z-index: 2;
}
.macros-edit-container {
z-index: 1;
display: inline;
position: absolute;
}
.btn-macros:not(:hover) .macros-edit-controls {
display: none;
}
.btn-macros:hover .macros-edit-controls {
display: flex;
}
.macros-controls-container {
display: flex;
flex-direction: column;
width: fit-content;
}
.macros-edit-controls {
}
.macros-edit-container {
display: flex;
flex-direction: row;
}
.macros-container {
display: flex;
flex-direction: row;
overflow: auto;
align-items: center;
}
.btn-send {
height: min-content;
}
.ipt-chat {
height: min-content;
border: 0 none transparent;
}
.ipt-chat:focus {
border: 0 none transparent;
outline: 0 none;
}
.btn-macros {
display: inline;
visibility: visible;
background: none;
border: none;
color: white;
margin-left: 10px;
}
.btn-macros-toggle {
margin-left: 10px;
margin-right: 10px;
height: min-content;
text-align: center;
}
.btn-macros-edit-toggle {
margin-left: 10px;
margin-right: 10px;
height: min-content;
text-align: center;
}
.btn-macros-add {
height: min-content;
margin-left: 10px;
margin-right: 10px;
text-align: center;
}
.macros-hidden {
visibility: hidden;
}
.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;
align-items: center;
}
.actions-layout {
display: flex;
max-width: 50%;
justify-items: start;
flex-direction: column;
overflow: visible;
}
.action-track {
display: flex;
flex-direction: row;
flex-wrap: wrap;
overflow: visible;
}
.pending {
background: #666 !important;
}
.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;
}
.room {
z-index: 0;
}
.selectable-menu {
position: absolute;
display: inline;
z-index: 1;
background: #336;
border: 1px white solid;
}
.selectable-menu-item:hover {
background: #666;
cursor: pointer;
}
.menu-container {
height: fit-content;
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 displayElts) {
const div = document.createElement("div")
div.className = elt
allEltsDiv.push(div)
}
allEltsDiv.forEach(div => hey.append(div))
for(let idx in allElts) {
updateElement(document, allElts[idx], allEltsDiv[displayElts.indexOf(allElts[idx])])
}
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)
}
setRawVisible(rawVisible)
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)