|
// ==UserScript== |
|
// @name Untrusted LOGgy |
|
// @namespace https://yanrishatum.ru |
|
// @version 1.1-99 |
|
// @description Improve log reading experience for Untrusted |
|
// @author Yanrishatum |
|
// @match *://*.playuntrusted.com/opsec/* |
|
// @match *://www.playuntrusted.com/manual/skills/ |
|
// @grant GM_xmlhttpRequest |
|
// @grant GM.xmlHttpRequest |
|
// @connect playuntrusted.com |
|
// ==/UserScript== |
|
(function() { |
|
var untrusted = {}; |
|
let skills = {}; |
|
let skillsLoading = false; |
|
if (localStorage["untrusted_skills"]) |
|
skills = JSON.parse(localStorage["untrusted_skills"]); |
|
else |
|
fetchSkills(); |
|
async function fetchSkills() { |
|
skillsLoading = true; |
|
if (typeof GM !== "undefined") |
|
GM_xmlhttpRequest = GM.xmlHttpRequest; |
|
// DOES NOT WORK: Servers don't have CORS properly configured |
|
// let xhr = new XMLHttpRequest(); |
|
// xhr.responseType = "document"; |
|
// xhr.open("GET", "https://www.playuntrusted.com/manual/skills/"); |
|
// await new Promise((r) => { xhr.onload = r; xhr.send(); }); |
|
// cacheSkills(xhr.responseXML!); |
|
try { |
|
const docStr = await new Promise((r) => { |
|
GM_xmlhttpRequest({ method: "GET", url: "https://www.playuntrusted.com/manual/skills/", onload: (res) => r(res.responseText) }); |
|
}); |
|
const doc = new DOMParser().parseFromString(docStr, "text/html"); |
|
cacheSkills(doc); |
|
} |
|
catch (e) { |
|
console.log(e); |
|
// Not in userscript |
|
} |
|
skillsLoading = false; |
|
// TODO: Force update |
|
} |
|
function cacheSkills(doc) { |
|
skillsLoading = true; |
|
console.log(doc); |
|
for (const hr of doc.querySelectorAll("#skillstable>hr")) { |
|
const left = hr.nextElementSibling; |
|
const right = left.nextElementSibling; |
|
const idEl = left.querySelector("a"); |
|
const nameEl = left.querySelector("h1"); |
|
idEl.remove(); |
|
nameEl.remove(); |
|
const skill = { |
|
id: idEl.getAttribute("name"), |
|
name: nameEl.textContent.trim(), |
|
description: left.innerHTML, |
|
img: right.querySelector("img").src, |
|
stats: right.querySelector(":scope>p").innerHTML |
|
}; |
|
left.prepend(nameEl); |
|
left.prepend(idEl); |
|
skills[skill.id] = skill; |
|
} |
|
localStorage["untrusted_skills"] = JSON.stringify(skills); |
|
console.log("Finished skill caching"); |
|
// TODO: Force update |
|
} |
|
if (location.pathname == '/manual/skills/') { |
|
cacheSkills(document); |
|
throw "Early exit: Manual page, only need to fetch skills"; |
|
} |
|
const roles = {}; |
|
function makeRole(name, abbr, sabbr, fact) { |
|
var r = { name: name, abbr: abbr, short_abbr: sabbr, faction: fact }; |
|
roles[name] = r; |
|
} |
|
// Netsec |
|
makeRole('Operation Leader', 'OL', ' OL ', 'NETSEC'); |
|
makeRole('Original Operation Leader', 'OOL', 'OOL ', 'NETSEC'); |
|
makeRole('Sociopath Operation Leader', 'SOL', 'SOL ', 'NETSEC'); |
|
makeRole('CCTV Specialist', 'CCTV', 'CCTV', 'NETSEC'); |
|
makeRole('Enforcer', 'ENFO', 'ENFO', 'NETSEC'); // NO OFFICIAL ABBR |
|
makeRole('Inside Man', 'IM', ' IM ', 'NETSEC'); |
|
makeRole('Analyst', 'ANAL', 'ANAL', 'NETSEC'); // NO OFFICIAL ABBR |
|
makeRole('Network Specialist', 'NS', ' NS ', 'NETSEC'); |
|
makeRole('Social Engineer', 'SE', ' SE ', 'NETSEC'); |
|
makeRole('Blackhat', 'BH', ' BH ', 'NETSEC'); |
|
makeRole('Improvised Hacker', 'IH', ' IH ', 'NETSEC'); |
|
makeRole('Spearphisher', 'SPH', ' SP ', 'NETSEC'); |
|
// Neutral |
|
makeRole('Bounty Hunter', 'BOHU', 'BOHU', "Neutral"); |
|
makeRole('Corupt Detective', 'CD', ' CD ', "Neutral"); // NO OFFICIAL ABBR |
|
makeRole('Double-crosser', 'DC', ' DC ', "Neutral"); |
|
makeRole('Journalist', 'JOURNO', 'JOUR', "Neutral"); |
|
makeRole('Loose Cannon', 'LC', ' LC ', "Neutral"); // Technically AGENT/NETSEC based on RNG |
|
makeRole('Loose Cannon (AGENT)', 'LC', ' LC ', "AGENT"); |
|
makeRole('Loose Cannon (NETSEC)', 'LC', ' LC ', "NETSEC"); |
|
makeRole('Script Kiddie', 'SK', ' SK ', "Neutral"); |
|
makeRole('Panicked Blabbermouth', 'PB', ' PB ', "Neutral"); |
|
makeRole('Resentful Criminal', 'RC', ' RC ', "Neutral"); |
|
makeRole('Sociopath', 'SOCIO', 'SOCI', "Neutral"); |
|
makeRole('Rival Hacker', 'RH', ' RH ', "Neutral"); |
|
// Agent |
|
makeRole('Agent Leader', 'AL', ' AL ', "AGENT"); |
|
makeRole('Field Agent', 'FA', ' FA ', "AGENT"); |
|
makeRole('Forensics Specialist', 'FS', ' FS ', "AGENT"); |
|
makeRole('Mole (Converted Field Ops)', 'MOLE(F)', 'MILF', "AGENT"); // NO OFFICIAL ABBR |
|
makeRole('Mole (Converted Inv.)', 'MOLE(I)', 'MOLI', "AGENT"); // NO OFFICIAL ABBR |
|
makeRole('Mole (Converted Offensive)', 'MOLE(O)', 'MOLO', "AGENT"); // NO OFFICIAL ABBR |
|
makeRole('Mole (Unknown Class)', 'MOLE(D)', 'MOLD', "AGENT"); // In case I didn't cover some mole conversion |
|
makeRole('Runaway Snitch', 'RS', ' RS ', "AGENT"); |
|
makeRole('unknown', '?', ' ?? ', "Neutral"); |
|
{ // CSS |
|
const css = ` |
|
/* #include-css(../css/untrusted_log.css) */ |
|
.col-agent { |
|
color: #fffeb2; |
|
} |
|
|
|
.col-netsec { |
|
color: #ffd5de; |
|
} |
|
|
|
.col-neutral { |
|
color: #cae4f6; |
|
} |
|
|
|
.ip-ttip { |
|
text-decoration: underline; |
|
} |
|
|
|
.skill { |
|
display: grid; |
|
grid-template: 'a a c' |
|
'b b d'; |
|
gap: 5px; |
|
white-space: normal; |
|
max-width: 600px; |
|
} |
|
|
|
.skill > h3 { |
|
grid-area: a; |
|
justify-self: center; |
|
align-self: center; |
|
} |
|
|
|
.skill > div:nth-child(2) { |
|
grid-area: b; |
|
} |
|
|
|
.skill > img { |
|
grid-area: c; |
|
justify-self: end; |
|
width: 96px; |
|
} |
|
|
|
.skill > div:last-child { |
|
grid-area: d; |
|
} |
|
|
|
.options label { |
|
white-space: nowrap; |
|
} |
|
|
|
.options select { |
|
background: black; |
|
color: white; |
|
} |
|
|
|
.options input[type='checkbox'] { |
|
width: 1em; |
|
height: 1em; |
|
background: black; |
|
border: 1px solid white; |
|
appearance: none; |
|
vertical-align: text-bottom; |
|
transition: background-color 0.1s ease; |
|
cursor: pointer; |
|
} |
|
|
|
.options input[type='checkbox']:hover { |
|
background-color: #272727; |
|
} |
|
|
|
.options input[type='checkbox']:checked { |
|
background-color: white; |
|
} |
|
|
|
.options input[type='checkbox']:checked:hover { |
|
background-color: #c4c4c4; |
|
} |
|
|
|
.tooltip { |
|
position: absolute; |
|
display: none; |
|
color: #fff; |
|
padding: 8px; |
|
margin: 0; |
|
} |
|
|
|
.event-target-name { |
|
margin-right: 1ch; |
|
display: inline-flex; |
|
flex-direction: row; |
|
} |
|
|
|
.event-target-name .name { |
|
order: 1; |
|
} |
|
|
|
.event-target-name .abbr { |
|
order: 2; |
|
} |
|
|
|
.abbr { |
|
white-space: nowrap; |
|
} |
|
|
|
.events-align-left .event-align { |
|
text-align: left !important; |
|
} |
|
|
|
.show-abbr-short.events-align-left .event-target-name .abbr { |
|
order: 0; |
|
} |
|
|
|
.abbr-short, .abbr-full { |
|
display: none; |
|
} |
|
|
|
.show-abbr-short .abbr-short { |
|
display: initial; |
|
} |
|
|
|
.show-abbr-full .abbr-full { |
|
display: initial; |
|
} |
|
|
|
.show-abbr-none .abbr { |
|
display: none; |
|
} |
|
|
|
.color-abbrs-faction .abbr.agent { |
|
color: #fffeb2; |
|
} |
|
|
|
.color-abbrs-faction .abbr.netsec { |
|
color: #ffd5de; |
|
} |
|
|
|
.color-abbrs-faction .abbr.neutral { |
|
color: #cae4f6; |
|
} |
|
|
|
.compact-chat .broadcast pre, .compact-chat .chat-message pre { |
|
margin: 0; |
|
padding: 4px 10px; |
|
white-space: normal; |
|
} |
|
|
|
.compact-chat .broadcast pre { |
|
padding-left: 24px; |
|
} |
|
|
|
.compact-chat .chat-message > td:first-child > table { |
|
margin-bottom: 0; |
|
display: flex; |
|
align-items: center; |
|
} |
|
|
|
.compact-chat .chat-message > td:first-child > table td { |
|
margin: 0 !important; |
|
padding: 0; |
|
} |
|
|
|
.compact-chat .chat-message > td:first-child > table td:nth-child(2) { |
|
flex-grow: 1; |
|
text-align: center; |
|
} |
|
|
|
.compact-chat .chat-message > td:first-child > table tbody, .compact-chat .chat-message > td:first-child > table tr { |
|
display: contents; |
|
} |
|
|
|
.compact-chat .chat-message img { |
|
width: 24px !important; |
|
height: 24px !important; |
|
} |
|
`; |
|
const style = document.querySelector("#untrusted-log-css") ?? document.createElement("style"); |
|
style.id = "#untrusted-log-css"; |
|
style.textContent = css; |
|
document.head.appendChild(style); |
|
} |
|
const OPTS_KEY = "untrusted_opts"; |
|
// Options menu |
|
const options = { |
|
abbrs: "short", |
|
colorCode: "faction", |
|
leftAlignEvents: true, |
|
compactChat: true, |
|
}; |
|
if (localStorage[OPTS_KEY]) |
|
Object.assign(options, JSON.parse(localStorage[OPTS_KEY])); |
|
function saveOpts() { localStorage[OPTS_KEY] = JSON.stringify(options); applyOpts(); } |
|
{ |
|
const optsCont = document.querySelector("h1").nextElementSibling.nextElementSibling; |
|
const panel = document.createElement("div"); |
|
panel.classList.add("options"); |
|
panel.innerHTML = ` |
|
<hr/> |
|
<div class="warn">Note: You need to realod the page for some changes to apply!</div> |
|
<label>Display abbreviations: <select id="abbr_mode"> |
|
<option value="none">Hidden</option> |
|
<option value="short">Short (4 chars)</option> |
|
<option value="full">Full (variable length)</option> |
|
</select></label> |
|
<label>Abbreviation colors: <select id="color_code"> |
|
<option value="none">White</option> |
|
<option value="faction">By faction</option> |
|
<option value="name">By name (DOES NOT WORK)</option> |
|
</select></label> |
|
<label><input type="checkbox" id="events_left_align"/>Align events on the left (readability)</label> |
|
<label><input type="checkbox" id="compact_chat"/>Compact chat</label> |
|
`; |
|
function bindSelect(key, query) { |
|
const sel = panel.querySelector(query); |
|
sel.value = options[key]; |
|
sel.onchange = () => { options[key] = sel.value; saveOpts(); }; |
|
} |
|
function bindCheck(key, query) { |
|
const check = panel.querySelector(query); |
|
check.checked = options[key]; |
|
check.onchange = () => { options[key] = check.checked; saveOpts(); }; |
|
} |
|
bindSelect("abbrs", "#abbr_mode"); |
|
bindSelect("colorCode", "#color_code"); |
|
bindCheck("leftAlignEvents", "#events_left_align"); |
|
bindCheck("compactChat", "#compact_chat"); |
|
optsCont.appendChild(panel); |
|
} |
|
function applyOpts() { |
|
document.body.classList.toggle('show-abbr-short', options.abbrs == "short"); |
|
document.body.classList.toggle('show-abbr-full', options.abbrs == "full"); |
|
document.body.classList.toggle('show-abbr-none', options.abbrs == "none"); |
|
document.body.classList.toggle('color-abbrs-none', options.colorCode == "none"); |
|
document.body.classList.toggle('color-abbrs-faction', options.colorCode != "none"); |
|
document.body.classList.toggle('color-abbrs-name', options.colorCode == "name"); |
|
document.body.classList.toggle("events-align-left", options.leftAlignEvents); |
|
document.body.classList.toggle('compact-chat', options.compactChat); |
|
} |
|
applyOpts(); |
|
const players = {}; |
|
let hackDuration = 7; |
|
let ol = null; |
|
let root = null; |
|
class Player { |
|
player; // Player name |
|
level; // Player level |
|
handle; // Player in-game handle (colors) |
|
item; // Held item |
|
img; // Avatar |
|
role; |
|
disguisedAs; |
|
skills; |
|
constructor(el) { |
|
if (el) { |
|
this.player = el.querySelector("div>a").textContent; |
|
this.level = el.querySelector(":scope>div:last-child").firstChild.textContent; |
|
this.handle = el.querySelector("div>div>div").textContent.replace(/^as "(.+)"$/, "$1"); |
|
this.item = el.querySelector("div>div>div:last-child").textContent; |
|
this.img = el.querySelector("img").src; |
|
} |
|
else { |
|
this.player = 'Unknown'; |
|
this.level = 'Level -'; |
|
this.handle = 'Unknown'; |
|
this.item = 'All the items'; |
|
this.img = undefined; |
|
} |
|
this.role = roles['unknown']; |
|
this.disguisedAs = null; |
|
this.skills = []; |
|
players[this.handle] = this; |
|
} |
|
abbr(pre = '', post = '') { |
|
return `<span class='abbr ${this.role.faction.toLowerCase()} name-${this.handle.substr(this.handle.indexOf(".") + 1).toLowerCase()}'>${pre}<span class='abbr-short'>[${this.role.short_abbr}]</span><span class='abbr-full'>[${this.role.abbr}]</span>${post}</span>`; |
|
} |
|
image(size) { |
|
if (this.img) |
|
return `<img style='width:${size}px;height:${size}px;' src='${this.img}'>`; |
|
return ''; |
|
} |
|
toString() { |
|
let base = `${this.image(48)} ${this.player} as <b>${this.role.name}</b>`.trimStart(); |
|
if (this.disguisedAs) |
|
base += ` <i>(${this.disguisedAs.name})</i>`; |
|
return base; |
|
} |
|
} |
|
// Make players |
|
new Player(null); |
|
Array.from(document.querySelectorAll("h1")).find(e => e.textContent == "OPSEC OPERATIVES LIST").nextElementSibling.nextElementSibling.querySelectorAll("td").forEach(el => new Player(el)); |
|
; |
|
; |
|
const TOPO_STEP_X = 98; |
|
const TOPO_STEP_Y = 78; |
|
const TOPO_SIZE = 48; |
|
const TOPO_CENTER = 24; |
|
const SERVER_SIZE_HACKED = 3550; |
|
const SERVER_SIZE_UNHACKED = 3886; |
|
const COMPUTER_SIZE_HACKED = 2906; |
|
const COMPUTER_SIZE_UNHACKED = 2990; |
|
const ipRegex = /\d+\.\d+\.\d+\.\d+/; |
|
function topoImg(src) { |
|
var i = new Image(TOPO_SIZE, TOPO_SIZE); |
|
i.src = src; |
|
i.loading = "eager"; |
|
return i; |
|
} |
|
// Had to embed to avoid topo breaking |
|
const topoImages = { |
|
"computer": topoImg("data:image/png;base64,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"), |
|
"computer_hacked": topoImg("data:image/png;base64,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"), |
|
"server": topoImg("data:image/png;base64,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"), |
|
"server_hacked": topoImg("data:image/png;base64,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") |
|
}; |
|
class Topology { |
|
// lines: HTMLCanvasElement; |
|
canvas; |
|
ctx; |
|
nodes; |
|
nodesByPos; |
|
conns; |
|
constructor() { |
|
let lines = document.querySelector("#topology"); |
|
this.canvas = document.createElement("canvas"); |
|
this.canvas.width = 4 * TOPO_STEP_X - TOPO_CENTER; |
|
this.canvas.height = 3 * TOPO_STEP_Y - TOPO_CENTER; |
|
this.ctx = this.canvas.getContext("2d"); |
|
this.nodes = []; |
|
this.nodesByPos = []; |
|
this.conns = []; |
|
let w = 0, h = 0; |
|
for (const img of lines.nextElementSibling.nextElementSibling.querySelectorAll("img")) { |
|
const node = { |
|
ip: "000.000.000.000", |
|
id: img.id, |
|
connections: [], |
|
x: parseFloat(img.style.marginLeft) / TOPO_STEP_X, |
|
y: parseFloat(img.style.marginTop) / TOPO_STEP_Y, |
|
kind: img.src.length == SERVER_SIZE_HACKED || img.src.length == SERVER_SIZE_UNHACKED ? "server" : "computer", |
|
hacked: false |
|
}; |
|
if (w < node.x) |
|
w = node.x; |
|
if (h < node.y) |
|
h = node.y; |
|
// const imgName = node.kind + (img.src.length == SERVER_SIZE_HACKED || img.src.length == COMPUTER_SIZE_HACKED ? "_hacked" : "") as ImageNames; |
|
// if (!this.images[imgName]) this.images[imgName] = img; |
|
this.nodes.push(node); |
|
if (!this.nodesByPos[node.x]) |
|
this.nodesByPos[node.x] = []; |
|
this.nodesByPos[node.x][node.y] = node; |
|
} |
|
this.canvas.width = (w + 1) * TOPO_STEP_X - TOPO_CENTER; |
|
this.canvas.height = (h + 1) * TOPO_STEP_Y - TOPO_CENTER; |
|
// Analyze topology connections |
|
let from = null, x, y; |
|
for (const line of lines.nextElementSibling.textContent.split("\n")) { |
|
let tmp = /ctx.moveTo\((\d+),(\d+)\);/.exec(line); |
|
if (tmp) { |
|
x = (parseFloat(tmp[1]) - 24) / TOPO_STEP_X; |
|
y = (parseFloat(tmp[2]) - 24) / TOPO_STEP_Y; |
|
from = this.nodesByPos[x][y]; |
|
} |
|
else if ((tmp = /ctx.lineTo\((\d+),(\d+)\);/.exec(line))) { |
|
x = (parseFloat(tmp[1]) - 24) / TOPO_STEP_X; |
|
y = (parseFloat(tmp[2]) - 24) / TOPO_STEP_Y; |
|
const node = this.nodesByPos[x][y]; |
|
const conn = { a: from, b: node }; |
|
from.connections.push(conn); |
|
node.connections.push(conn); |
|
this.conns.push(conn); |
|
} |
|
} |
|
// Analyze topology IPs |
|
const iter = lines.nextElementSibling.nextElementSibling.querySelector("script").textContent.split("\n").map(s => s.trim()).filter(s => s.includes("logs for:") || s.includes(".mouseover"))[Symbol.iterator](); |
|
let ival; |
|
do { |
|
ival = iter.next(); |
|
if (ival.done) |
|
break; |
|
const node = this.nodes.find(n => n.id == "node" + parseInt(/"#node(\d+)"/.exec(ival.value)[1])); |
|
ival = iter.next(); |
|
if (node) |
|
node.ip = ipRegex.exec(ival.value)[0]; |
|
} while (!ival.done); |
|
} |
|
hack(ip) { |
|
const node = this.nodes.find(n => n.ip == ip); |
|
if (node) |
|
node.hacked = true; |
|
} |
|
rollback(ip) { |
|
const node = this.nodes.find(n => n.ip == ip); |
|
if (node) |
|
node.hacked = false; |
|
} |
|
paint(ip) { |
|
const hackState = new Map(); |
|
for (const n of this.nodes) |
|
hackState.set(n.ip, n.hacked); |
|
const ctx = this.ctx; |
|
const canvas = this.canvas; |
|
const conns = this.conns; |
|
const nodes = this.nodes; |
|
return { |
|
toString() { |
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
// Draw connections |
|
for (const conn of conns) { |
|
ctx.beginPath(); |
|
ctx.strokeStyle = hackState.get(conn.a.ip) && hackState.get(conn.b.ip) ? "#00ff01" : "#fff"; |
|
ctx.lineWidth = 2; |
|
ctx.moveTo(conn.a.x * TOPO_STEP_X + TOPO_CENTER, conn.a.y * TOPO_STEP_Y + TOPO_CENTER); |
|
ctx.lineTo(conn.b.x * TOPO_STEP_X + TOPO_CENTER, conn.b.y * TOPO_STEP_Y + TOPO_CENTER); |
|
ctx.stroke(); |
|
ctx.closePath(); |
|
} |
|
// Draw nodes |
|
for (const node of nodes) { |
|
const img = topoImages[(node.kind + (hackState.get(node.ip) ? "_hacked" : ""))]; |
|
if (node.ip == ip) { |
|
ctx.beginPath(); |
|
ctx.strokeStyle = "#00ff01"; |
|
ctx.roundRect(node.x * TOPO_STEP_X, node.y * TOPO_STEP_Y, TOPO_SIZE, TOPO_SIZE, 4); |
|
ctx.stroke(); |
|
ctx.closePath(); |
|
} |
|
if (!img) |
|
console.log("WTF"); |
|
ctx.drawImage(img, node.x * TOPO_STEP_X, node.y * TOPO_STEP_Y, TOPO_SIZE, TOPO_SIZE); |
|
} |
|
return canvas.toDataURL(); |
|
} |
|
}; |
|
} |
|
} |
|
const topo = new Topology(); |
|
// Process log |
|
const logStart = Array.from(document.querySelectorAll("h1")).find(e => e.textContent == "OPSEC LOG"); |
|
const logTable = logStart.nextElementSibling.nextElementSibling.querySelector("table tbody"); |
|
function* logIter() { |
|
let el = logTable.firstElementChild; |
|
while (el.nextElementSibling) { |
|
if (el.textContent == '' && el.nextElementSibling) { |
|
el = el.nextElementSibling; // Skip empty ones |
|
continue; |
|
} |
|
yield el; |
|
el = el.nextElementSibling; |
|
} |
|
return el; |
|
} |
|
let stage = 'roles'; |
|
function parseEvent(ev) { |
|
const privateExec = /^\(([^)]+)\)\s(.+)$/.exec(ev); |
|
if (privateExec) { |
|
return { |
|
isPrivate: true, |
|
target: players[privateExec[1]], |
|
message: privateExec[2] |
|
}; |
|
} |
|
else { |
|
// TODO |
|
return { |
|
isPrivate: false, |
|
target: players['Everyone'], |
|
message: ev |
|
}; |
|
} |
|
} |
|
const ttip = document.createElement("pre"); |
|
ttip.classList.add("tooltip"); |
|
document.body.appendChild(ttip); |
|
let ttipCounter = 0; |
|
/** @param {HTMLElement} el */ |
|
function addTooltip(el, text) { |
|
el.addEventListener("mouseenter", (e) => { |
|
ttipCounter++; |
|
const bbox = el.getBoundingClientRect(); |
|
ttip.style.left = (bbox.left + window.scrollX) + "px"; |
|
ttip.style.top = (window.scrollY + bbox.bottom + 2) + "px"; |
|
ttip.style.display = 'block'; |
|
ttip.innerHTML = text.toString(); |
|
}); |
|
el.addEventListener("mouseleave", () => { |
|
ttipCounter--; |
|
if (ttipCounter <= 0) { |
|
ttipCounter = 0; |
|
ttip.style.display = 'none'; |
|
} |
|
}); |
|
} |
|
function addSkillTooltip(el, id) { |
|
const disp = { |
|
toString() { |
|
if (skillsLoading) |
|
return 'Skill list is still being loaded!'; |
|
const skill = skills[id]; |
|
if (!skill) |
|
return "Unknown skill!"; |
|
return ` |
|
<div class="skill"> |
|
<h3>${skill.name}</h3> |
|
<div>${skill.description}</div> |
|
<img src="${skill.img}"> |
|
<div>${skill.stats}</div> |
|
</div>`.trim(); |
|
} |
|
}; |
|
addTooltip(el, disp); |
|
} |
|
function processIP(text) { |
|
let ip = ipRegex.exec(text); |
|
if (ip) { |
|
const left = text.substring(0, ip.index); |
|
const right = text.substring(ip.index + ip[0].length); |
|
const disp = document.createElement("span"); |
|
disp.textContent = ip[0]; |
|
disp.classList.add("ip-ttip"); |
|
addTooltip(disp, `<img src="${topo.paint(ip[0])}">`); |
|
let result = [left, disp].concat(processIP(right)); |
|
return result; |
|
} |
|
else |
|
return [text]; |
|
} |
|
let line, ev; |
|
console.time("Log parser"); |
|
for (const logLine of logIter()) { |
|
switch (stage) { |
|
case 'roles': |
|
line = logLine.textContent.trim(); |
|
if (line == 'Preparation Night') { |
|
stage = 'chat'; |
|
break; |
|
} |
|
let durExec = /The hack must be completed within (\d+) days?./.exec(line)?.[1]; |
|
if (durExec) { |
|
hackDuration = parseInt(durExec); |
|
break; |
|
} |
|
ev = parseEvent(line); |
|
// console.log(ev, line); |
|
const player = ev.target; |
|
const bEl = logLine.querySelector("b"); |
|
let reg; |
|
if (bEl) { |
|
player.role = roles[bEl.textContent.trim()] ?? roles['unknown']; |
|
if (player.role.name == 'Operation Leader') |
|
ol = player; // Assign OL for anonymous messages |
|
} |
|
else if ((reg = /^You have a cover as '([^']+)'/.exec(ev.message))) { |
|
player.disguisedAs = roles[reg[1]]; |
|
} |
|
else if ((reg = /'([^']+)' skill.$/.exec(ev.message))) { |
|
player.skills.push(reg[1]); |
|
} |
|
if (player.role.name == 'Loose Cannon') { |
|
if (ev.message.includes('make sure AGENT wins')) |
|
player.role = roles['Loose Cannon (AGENT)']; |
|
else if (ev.message.includes('make sure NETSEC wins')) |
|
player.role = roles['Loose Cannon (NETSEC)']; |
|
} |
|
break; |
|
case 'chat': |
|
if (logLine.querySelector(":scope>td[colspan='2']>pre")) { |
|
// Broadcast |
|
logLine.classList.add("broadcast"); |
|
const sender = logLine.querySelector("b"); |
|
if (sender) |
|
addTooltip(sender, ol.toString()); |
|
} |
|
else if (logLine.querySelector(":scope>td[colspan='2']")?.textContent?.trim()?.startsWith("Events")) { |
|
// End of chat |
|
stage = 'events'; |
|
break; |
|
} |
|
else { |
|
if (!logLine.querySelector(":scope>td[colspan='2']")) |
|
logLine.classList.add("chat-message"); |
|
// Add tooltips for all users |
|
for (let userImg of logLine.querySelectorAll("img")) { |
|
let name = userImg.parentElement.textContent.trim(); |
|
if (name.endsWith(":")) |
|
name = name.substring(0, name.length - 1); |
|
const player = players[name]; |
|
if (player) { |
|
userImg.insertAdjacentHTML("afterend", player.abbr('', ' ')); |
|
addTooltip(userImg.parentElement, player.toString()); |
|
} |
|
} |
|
const chatText = logLine.querySelector("td:nth-child(2)>pre, td:nth-child(2)>i") ?? logLine.querySelector("td:nth-child(2)"); |
|
if (chatText?.lastChild?.nodeType == Node.TEXT_NODE) { |
|
const proc = processIP(chatText.lastChild.textContent); |
|
if (proc.length > 1) { |
|
chatText.lastChild.remove(); |
|
chatText.append(...proc); |
|
} |
|
else { |
|
//TODO: on Dr.X |
|
} |
|
} |
|
} |
|
break; |
|
case 'events': |
|
line = logLine.textContent.trim(); |
|
if (line.startsWith("Night") || line.startsWith("Day")) { |
|
// End of events |
|
stage = 'chat'; |
|
break; |
|
} |
|
const ctd = logLine.querySelector("td[style*='text-align:center']"); |
|
if (ctd) |
|
ctd.classList.add("event-align"); |
|
ev = parseEvent(line); |
|
if (ev.isPrivate) { |
|
const player = ev.target; |
|
// Reclass |
|
if (ev.message.includes('and stolen their precious, precious Operation Leader identity')) { |
|
player.role = roles['Sociopath Operation Leader']; |
|
ol.role = roles['Original Operation Leader']; |
|
ol = player; |
|
} |
|
else if (ev.message.includes('you are now the new Operation Leader')) { |
|
player.role = roles['Operation Leader']; |
|
ol.role = roles['Original Operation Leader']; // Defo gonna conflict if socio is in play |
|
ol = player; |
|
} |
|
else if (ev.message.includes('and became an Improvised Hacker')) { |
|
player.role = roles['Improvised Hacker']; |
|
} |
|
else if (ev.message.includes('You are panicking - NETSEC is out to get you')) { |
|
player.role = roles['Runaway Snitch']; |
|
} |
|
else if (ev.message.includes('All of the death around you made you question your life choices')) { |
|
player.role = roles['Panicked Blabbermouth']; |
|
} |
|
else if (ev.message.includes('you are now the Field Agent') || ev.message.includes('You have been promoted to Field Agent')) { |
|
player.role = roles['Field Agent']; |
|
} |
|
else if (ev.message.includes('Do not reveal that you\'ve become a mole')) { |
|
switch (player.role.name) { |
|
case 'CCTV Specialist': |
|
case 'Enforcer': |
|
case 'Inside Man': |
|
case 'Loose Cannon': |
|
case 'Loose Cannon (AGENT)': |
|
case 'Loose Cannon (NETSEC)': |
|
player.role = roles['Mole (Converted Field Ops)']; |
|
break; |
|
case 'Analyst': |
|
case 'Network Specialist': |
|
case 'Social Engineer': |
|
case 'Double-crosser': |
|
player.role = roles['Mole (Converted Inv.)']; |
|
break; |
|
case 'Improvised Hacker': |
|
case 'Spearphisher': |
|
case 'Panicked Blabbermouth': |
|
case 'Resentful Criminal': |
|
case 'Rival Hacker': |
|
player.role = roles['Mole (Converted Offensive)']; |
|
break; |
|
default: |
|
player.role = roles['Mole (Unknown Class)']; |
|
break; |
|
} |
|
} |
|
else if (ev.message.includes('and you have been granted root access')) { |
|
root = player; |
|
} |
|
const td = logLine.querySelector("td"); |
|
const nameSub = document.createElement("span"); |
|
let disp = `<span class="name">(${player.handle})</span>${player.abbr('', '')}`; |
|
nameSub.innerHTML = disp; |
|
nameSub.classList.add("event-target-name"); |
|
addTooltip(nameSub, player.toString()); |
|
td.textContent = ""; |
|
td.append(nameSub, ...processIP(ev.message)); |
|
} |
|
else { |
|
if (ev.message.includes("NETSEC now has root privileges on")) { |
|
let ip = ipRegex.exec(ev.message); |
|
if (ip) |
|
topo.hack(ip[0]); |
|
} |
|
else if (ev.message.includes("Someone rolled back")) { |
|
let ip = ipRegex.exec(ev.message); |
|
if (ip) |
|
topo.rollback(ip[0]); |
|
} |
|
else if (ev.message.includes("The suspect was charged as Operation Leader") || ev.message.includes("Rumor is that the victim was a(n) Operation Leader")) { |
|
if (root) { |
|
ol = root; |
|
root = null; |
|
} |
|
} |
|
let data = processIP(ev.message); |
|
if (data.length > 1) { |
|
const td = logLine.querySelector("td"); |
|
td.innerHTML = ""; |
|
td.append(...data); |
|
} |
|
} |
|
} |
|
} |
|
console.timeEnd("Log parser"); |
|
console.time("Skill tooltips"); |
|
for (const skillLink of document.querySelectorAll("a[href*='manual/skills/#']")) { |
|
addSkillTooltip(skillLink, new URL(skillLink.href).hash.substring(1)); |
|
} |
|
console.timeEnd("Skill tooltips"); |
|
})() |