Skip to content

Instantly share code, notes, and snippets.

@Yanrishatum
Last active January 31, 2023 20:44
Show Gist options
  • Save Yanrishatum/8125378fb548e8a882a368c7ff5ee9ed to your computer and use it in GitHub Desktop.
Save Yanrishatum/8125378fb548e8a882a368c7ff5ee9ed to your computer and use it in GitHub Desktop.
Unstrusted LOGgy: UX tweaks for reading dem circus
// ==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");
})()

1.1-88/1.1-99

  • Fixed final topology with no unhacked server/laptop breaking stuff.

1.1-87

  • Add compact chat option
  • Make topology dimensions dynamic (4x4 grid for large lobby)

1.0-80

  • First public release
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment