Skip to content

Instantly share code, notes, and snippets.

@Telokis
Created February 21, 2024 10:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Telokis/c78fa98c3dd8280b0c4ef7eea6ff4897 to your computer and use it in GitHub Desktop.
Save Telokis/c78fa98c3dd8280b0c4ef7eea6ff4897 to your computer and use it in GitHub Desktop.
Telo's Adventure Land UI stuff

This is a small collection of UI things I made for Adventure Land.

ui_bank.ts

This one is the overall bank UI that's already been shown several times on Discord. Mine will print a nice item name when hovering over an entry and group items by .p modifier with an icon in the top right corner.

inject.ts

The inject.ts file is important, most of my modifications rely on it.

ui_extensions.ts

This file contains a lot of random modifications and adjustments I've made to the game UI for convenience/UX.
Several of them could/should probably move to the base game now that it's open source.
And some of them are shitty things that never properly worked but are still there! x)

Modules things

A small project I had to make a UI module system. Anyone would be able to define and share their own UIModule that a ModuleManager would then load and run.
Never finished it, though.

function escapeRegExp(str: string) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
}
function replaceAll(that: string, str: string | RegExp, newStr: string) {
// If a regex pattern
if (
str instanceof RegExp ||
Object.prototype.toString.call(str).toLowerCase() === "[object regexp]"
) {
return that.replace(str, newStr);
}
// If a string
return that.replace(new RegExp(escapeRegExp(str), "g"), newStr);
}
/** We call-wrap functions to avoid altering their prototype/name. */
function wrap(str: string) {
return `(${str})(...arguments)`;
}
export function wrapFunction(myFn: (...args: any) => any) {
return wrap(myFn.toString());
}
/** Backup of all non-modified functions to later restore them. */
const originalFunctions: Record<string, string> = {};
/** If true, throws an error if the operation fails. If false, silently ignores. */
const THROW_ON_FAIL = true;
/**
* Will inject code into a function defined in the parent object.
* The aim is to make it easier to customize the UI of the game.
* It is also safer because you don't have to look at a huge chunk
* of code to know what is being injected: it is a real function.
*
* @warning Since this will basically result in an eval, make sure you trust the injected code.
* Please be careful.
*
* @param {string} func Name of the function that should be modified in parent.
* For example, "render_item" will modify parent.render_item.
* @param {string} marker The code that your function should be injected before.
* Can be "$$END$$" to inject at the end of the function.
* If the marker is not found, no modification is done and
* the function will either throw or log a message depending
* on the value of THROW_ON_FAIL.
* @param {function} myFn The function you want to inject.
* This function will be stringified so it should be totally
* self-contained and not rely on closure AT ALL.
* Also be careful if you are bundling your code, your bundler might
* alter the function and make it not self-contained.
* @param {object?} defines Optional objects to have some kind of C-like defines.
* The keys of the object will be used to replace things in the function.
* For example, `{ MY_VAL: "hello" }` will replace all occurrences of the
* text "MY_VAL" with the stringified string "\"hello\"".
* Be careful because this replace is brutal and stupid, it will overwrite
* any match found.
*/
export function injectIntoParent(
func: keyof typeof parent & string,
marker: string,
myFn: (...args: any) => any,
injectAfter = false,
defines?: Record<string, any>,
) {
// @ts-ignore
let str = parent[func].toString();
let myWrapped = wrapFunction(myFn);
if (!(func in originalFunctions)) {
originalFunctions[func] = str;
}
if (defines) {
for (const [name, val] of Object.entries(defines)) {
if (typeof val === "function") {
myWrapped = myWrapped.replace(name, `(${val.toString()})`);
} else {
myWrapped = myWrapped.replace(name, JSON.stringify(val));
}
}
}
if (str.includes(marker)) {
if (!injectAfter) {
str = str.replace(marker, `${myWrapped}\n${marker}`);
} else {
str = str.replace(marker, `${marker}\n${myWrapped}`);
}
} else if (marker === "$$END$$") {
str += `${myWrapped}\n`;
} else {
if (THROW_ON_FAIL) {
throw new Error(`Unable to find ${JSON.stringify(marker)} in ${func}.`);
}
log(`Unable to find ${JSON.stringify(marker)} in ${func}.`, "red");
return;
}
/* eslint-disable no-new-func */
// @ts-ignore
const newFn = new Function(`return ${wrap(str)}`);
parent.smart_eval(`this[${JSON.stringify(func)}] = ${newFn.toString()};`);
}
/**
* Will inject code into a function defined in the parent object.
* The aim is to make it easier to customize the UI of the game.
* It is also safer because you don't have to look at a huge chunk
* of code to know what is being injected: it is a real function.
*
* @warning Since this will basically result in an eval, make sure you trust the injected code.
* Please be careful.
*
* @param {string} func Name of the function that should be modified in parent.
* For example, "render_item" will modify parent.render_item.
* @param {string} toReplace
* @param {string} replacement
*/
export function replaceInParentFunction(
func: keyof typeof parent & string,
toReplace: string,
replacement: string,
) {
// @ts-ignore
let str = parent[func].toString();
if (!(func in originalFunctions)) {
originalFunctions[func] = str;
}
if (str.includes(toReplace)) {
str = replaceAll(str, toReplace, replacement);
} else {
if (THROW_ON_FAIL) {
throw new Error(`Unable to find ${JSON.stringify(toReplace)} in ${func}.`);
}
log(`Unable to find ${JSON.stringify(toReplace)} in ${func}.`, "red");
return;
}
/* eslint-disable no-new-func */
// @ts-ignore
const newFn = new Function(`return ${wrap(str)}`);
parent.smart_eval(`this[${JSON.stringify(func)}] = ${newFn.toString()};`);
}
/**
* Will reset all modified function to their original code.
* PLEASE MAKE SURE TO CALL THIS FUNCTION IN YOUR `on_destroy` HANDLER!!!!
* If you don't call this, your UI will break the next time you stop and
* restart your code.
* To fix this broken state, you have to sign out and back in.
* A refresh is probably needed in the browser version, I don't know.
*/
export function resetParent() {
for (const [name, code] of Object.entries(originalFunctions)) {
parent.smart_eval(`this[${JSON.stringify(name)}] = ${code};`);
}
}
import { PromiseOrNot } from "../../types/PromiseOrNot";
/**
* A function that takes in a function and a number of milliseconds.
* The function will be called every ms milliseconds.
* @param fn - the function to call every ms milliseconds.
* @param ms - the number of milliseconds to wait between calls.
* @returns Stop function
*/
export function asyncInterval(fn: (...args: any[]) => PromiseOrNot<any>, ms: number) {
let stopped = false;
async function wrapper(): Promise<void> {
if (!stopped) {
try {
await fn();
} catch (err: any) {
console.error("timeout_loop error:", err);
setTimeout(wrapper, ms / 10);
return;
}
setTimeout(wrapper, ms);
}
}
wrapper();
return () => {
stopped = true;
};
}
import { UIModule, UIModuleContext, UIModuleMetadata } from "./UIModule";
const coordsUIModuleMetadata: UIModuleMetadata = {
id: "Telokis/Coords",
name: "Coordinates",
description: "Shows your coordinates.",
};
export class CoordsUIModule implements UIModule {
static metadata = coordsUIModuleMetadata;
metadata = coordsUIModuleMetadata;
setup(ctx: UIModuleContext) {
const $div = ctx.getOrCreateDiv("coords").css({
background: "black",
border: "solid gray",
borderWidth: "5px 5px",
height: "34px",
lineHeight: "34px",
fontSize: "30px",
textAlign: "center",
});
ctx.$("#bottomleftcorner").append($div);
}
update(ctx: UIModuleContext) {
const $div = ctx.getOrCreateDiv("coords");
$div.html(`(${character.real_x.toFixed(0)}, ${character.real_y.toFixed(0)})`);
}
}
/*
* This file is a test to see how the module system works and behaves.
*/
import { ModuleManager } from "./ModuleManager";
import { KillTrackerUIModule } from "./KillTracker.ui";
import { CoordsUIModule } from "./Coords.ui";
const moduleManager = new ModuleManager();
const oldOnDestroy = globalThis.on_destroy ?? on_destroy;
globalThis.on_destroy = function myOnDestroy() {
oldOnDestroy?.();
moduleManager.stop();
};
moduleManager.registerModule(KillTrackerUIModule);
moduleManager.registerModule(CoordsUIModule);
moduleManager.enable(KillTrackerUIModule.metadata.id);
moduleManager.enable(CoordsUIModule.metadata.id);
moduleManager.start();
import { ServerToClient_game_log } from "typed-adventureland";
import { UIModule, UIModuleContext, UIModuleMetadata } from "./UIModule";
const killTrackerUIModuleMetadata: UIModuleMetadata = {
id: "Crown/KillTracker",
name: "Kill Tracker",
description: "Tracks monster kills.",
};
export class KillTrackerUIModule implements UIModule {
static metadata = killTrackerUIModuleMetadata;
metadata = killTrackerUIModuleMetadata;
setup(ctx: UIModuleContext) {
ctx.addSocketListener("game_log", this.onGameLog.bind(this));
const $div = ctx.getOrCreateDiv("display").css({
background: "black",
border: "solid gray",
borderWidth: "5px 5px",
height: "34px",
lineHeight: "34px",
fontSize: "30px",
textAlign: "center",
});
$div.html("Nothing yet!");
ctx.$("#bottomrightcorner").append($div);
console.log("Adding", $div);
console.log("Parent", $("#bottomrightcorner"));
}
update(ctx: UIModuleContext) {
const elapsed = (Date.now() - this.killStart) / 1000;
const killsPerSec = this.kills / elapsed;
const dailyKillRate = killsPerSec * 60 * 60 * 24;
const $div = ctx.getOrCreateDiv("display");
$div.html(`Kills per day: ${dailyKillRate}`);
}
// Specific
kills = 0;
killStart = Date.now();
onGameLog(data: ServerToClient_game_log) {
if (typeof data === "string" && data.includes("killed a ")) {
this.kills++;
}
}
}
import { UIModuleConstructor, UIModuleContext } from "./UIModule";
export class ModuleContext implements UIModuleContext {
$: JQueryStatic;
uiModule: UIModuleConstructor;
socketHandlers: Array<[event: string, callback: (...args: any) => any]> = [];
jqueryElements: Map<string, JQuery<HTMLElement>> = new Map();
constructor(uiModule: UIModuleConstructor) {
this.uiModule = uiModule;
this.$ = parent.$;
}
getOrCreateDiv(name: string) {
const elem = $(`<div id="${this.uiModule.metadata.id}/${name}">`);
this.jqueryElements.set(name, elem);
return elem;
}
addSocketListener: Window["socket"]["on"] = (
event: string,
callback: (...args: any) => any,
) => {
// @ts-ignore
parent.socket.on(event, callback);
this.socketHandlers.push([event, callback]);
};
removeSocketListener: Window["socket"]["on"] = (
event: string,
callback: (...args: any) => any,
) => {
// @ts-ignore
parent.socket.off(event, callback);
const index = this.socketHandlers.findIndex(([e, c]) => event === e && callback === c);
this.socketHandlers.splice(index, 1);
};
cleanup() {
for (const [event, callback] of this.socketHandlers) {
// @ts-ignore
parent.socket.off(event, callback);
}
this.socketHandlers = [];
for (const elemName of this.jqueryElements.keys()) {
const fullName = `${this.uiModule.metadata.id}/${elemName}`.replace(
/[!"#$%&'()*+,./:;<=>?@[\]^`{|}~]/g,
"\\\\$&",
);
console.log("Removing element", fullName);
parent.smart_eval(`$("#${String(fullName)}").remove()`);
console.log("Removed element", fullName);
}
this.jqueryElements = new Map();
}
}
import { asyncInterval } from "./asyncInterval";
import { ModuleContext } from "./ModuleContext";
import { UIModule, UIModuleConstructor } from "./UIModule";
export class ModuleManager {
private enabledModules: Map<string, UIModule> = new Map();
private registeredModules: Map<string, UIModuleConstructor> = new Map();
private contexts: Map<string, ModuleContext> = new Map();
private loopStopper: (() => void) | null = null;
registerModule(uiModule: UIModuleConstructor) {
console.log(`Registering module ${uiModule.metadata.id}`);
this.registeredModules.set(uiModule.metadata.id, uiModule);
this.contexts.set(uiModule.metadata.id, new ModuleContext(uiModule));
}
enable(moduleId: string) {
const UIModuleToCreate = this.registeredModules.get(moduleId);
if (!UIModuleToCreate) {
console.error(`[enable] Module ${moduleId} not found.`);
return;
}
const context = this.contexts.get(moduleId);
if (!context) {
console.error(`[enable] Context not found for module ${moduleId}.`);
return;
}
const instance = new UIModuleToCreate();
this.enabledModules.set(moduleId, instance);
}
async start() {
if (this.loopStopper) {
console.error("Module manager already started.");
return;
}
console.log("Starting module manager");
await this.setupAll();
this.loopStopper = asyncInterval(async () => {
for (const [moduleId, instance] of this.enabledModules) {
const context = this.contexts.get(moduleId);
if (!context) {
console.error(`Context not found for module ${moduleId}.`);
return;
}
if (instance.update) {
instance.update(context);
}
}
}, 50);
}
async setupAll() {
const promises: Array<Promise<void>> = [];
for (const [moduleId, instance] of this.enabledModules) {
const context = this.contexts.get(moduleId);
if (!context) {
console.error(`[setupAll] Context not found for module ${moduleId}.`);
continue;
}
if (instance.setup) {
promises.push(Promise.resolve(instance.setup(context)));
}
}
return await Promise.all(promises);
}
cleanupAll() {
for (const [moduleId, instance] of this.enabledModules) {
const context = this.contexts.get(moduleId);
if (!context) {
console.error(`[cleanupAll] Context not found for module ${moduleId}.`);
continue;
}
if (instance.cleanup) {
instance.cleanup(context);
}
context.cleanup();
this.contexts.delete(moduleId);
}
}
updateAll() {
for (const [moduleId, instance] of this.enabledModules) {
const context = this.contexts.get(moduleId);
if (!context) {
console.error(`[updateAll] Context not found for module ${moduleId}.`);
return;
}
if (instance.update) {
instance.update(context);
}
}
}
stop() {
if (!this.loopStopper) {
console.log("Module manager already stopped.");
return;
}
console.log("Stopping module manager...");
console.log("Stopping loop...");
this.loopStopper();
this.loopStopper = null;
console.log("Loop stopped!");
console.log("Cleaning modules and contexts...");
this.cleanupAll();
console.log("Cleaning done!");
console.log("Module manager stopped!");
}
}
import { PromiseOrNot } from "../../types/PromiseOrNot";
export interface UIModuleContext {
$: JQueryStatic;
getOrCreateDiv(name: string): JQuery<HTMLElement>;
addSocketListener: Window["socket"]["on"];
removeSocketListener: Window["socket"]["on"];
}
export interface UIModuleMetadata {
id: string;
name: string;
description: string;
}
export interface UIModule {
metadata: UIModuleMetadata;
/**
* Called when the module is loaded so it can initialise itself.
*/
setup?(ctx: UIModuleContext): PromiseOrNot<void>;
/**
* Called when the module is loaded so it can initialise itself.
*
* @warning This function must absolutely be synchronous.
*/
cleanup?(ctx: UIModuleContext): void;
/**
* Called from the update loop. Do your redraws here.
*
* @warning This function should be synchronous.
*/
update?(ctx: UIModuleContext): void;
}
export interface UIModuleConstructor {
metadata: UIModuleMetadata;
new (): UIModule;
}
/**
* Originally shared by drippy on discord https://discord.com/channels/238332476743745536/243707345887166465/1041087432055066744
* we do not know who originally made it.
* number_e#1635 on discord wrote the initial snippet
* modified by thmsn to use more width and float item categories
* usage: load theese functions into your parent window and execute the following command while in the bank
* `renderBankItemsOverview()`.
* Will also use a globally defined `bankCache` variable.
*/
import { bankSpaceLeft } from "../helpers/bankCount"; // Retrieves the available number of bank slots
import { objectValues } from "../types/ObjectHelpers"; // Object.values but TS-typed
const tshirtNames = {
tshirt88: "Lucky", // Luck and all
tshirt9: "Manasteal", // Manasteal
tshirt3: "XP", // XP
tshirt8: "Attack MP", // Attack MP cost
tshirt7: "Armor piercing", // Armor piercing
tshirt6: "Res. piercing", // Res. piercing
tshirt4: "Speed", // Speed
};
export function niceItemName(item) {
if (!item) {
return "null";
}
const gItem = G.items[item.name];
let { name } = gItem;
if (item.name in tshirtNames) {
name = `${tshirtNames[item.name]} ${name}`;
}
if (item && item.p && G.titles[item.p]) {
name = `${G.titles[item.p].title} ${name}`;
} else if (item && item.p) {
name = `${item.p} ${name}`;
}
if (item.level) {
if (gItem.upgrade && item.level === 12) {
name += " +Z";
} else if (gItem.upgrade && item.level === 11) {
name += " +Y";
} else if (gItem.upgrade && item.level === 10) {
name += " +X";
} else if (gItem.compound && item.level === 7) {
name += " +R";
} else if (gItem.compound && item.level === 6) {
name += " +S";
} else if (gItem.compound && item.level === 5) {
name += " +V";
} else {
name += ` +${item.level}`;
}
}
if (item.q) {
name = `${item.q ?? 1}x ${name}`;
}
return name;
}
function renderItemsOverview(itemsByCategory) {
if (itemsByCategory.length > 0 && !Array.isArray(itemsByCategory[0])) {
itemsByCategory = [["Items", itemsByCategory]];
}
let html =
"<div style='border: 5px solid gray; background-color: black; padding: 10px; width: 90%, height:90%'>";
html += `<div style="
font-size: 32px;
margin-left: 8px;
margin-bottom: 10px;
">Bank slots left: ${bankSpaceLeft()}</div>`;
itemsByCategory.forEach(([category, items]) => {
html += `<div style='float:left; margin-left:5px;'><div class='gamebutton gamebutton-small' style='margin-bottom: 5px'>${category}</div>`;
html += "<div style='margin-bottom: 10px'>";
items.forEach((item) => {
let itemDiv = parent.item_container(
{
skin: G.items[item.name].skin,
onclick: `show_modal(render_item('html', {item: G.items['${
item.name
}'],name: '${item.name}',actual: ${JSON.stringify(item).replace(
/"/g,
"'",
)}}),{wrap: false})`,
},
item,
);
if (item.p) {
let corner = "";
switch (item.p) {
case "festive": {
corner = `<div class='trruui imu' style='border-color: grey; color:#79ff7e'>F</div></div></div>`;
break;
}
case "firehazard": {
corner = `<div class='trruui imu' style='border-color: grey; color:#f79b11'>H</div></div></div>`;
break;
}
case "glitched": {
corner = `<div class='trruui imu' style='border-color: grey; color:grey'>#</div></div></div>`;
break;
}
case "gooped": {
corner = `<div class='trruui imu' style='border-color: grey; color:#64B867'>G</div></div></div>`;
break;
}
case "legacy": {
corner = `<div class='trruui imu' style='border-color: grey; color:white''>L</div></div></div>`;
break;
}
case "lucky": {
corner = `<div class='trruui imu' style='border-color: grey; color:#00f3ff''>L</div></div></div>`;
break;
}
case "shiny": {
corner = `<div class='trruui imu' style='border-color: grey; color:#99b2d8''>S</div></div></div>`;
break;
}
case "superfast": {
corner = `<div class='trruui imu' style='border-color: grey; color:#c681dc''>U</div></div></div>`;
break;
}
default: {
corner = `<div class='trruui imu' style='border-color: black; color:grey''>?</div></div></div>`;
}
}
itemDiv = itemDiv.replace(/<\/div><\/div>$/, corner);
}
if (item.q > 999_999) {
itemDiv = itemDiv.replace(
`<div class='iqui'>${item.q}</div>`,
`<div class='iqui'>${Math.floor(item.q / 1_000_000)}M</div>`,
);
} else if (item.q > 9_999) {
itemDiv = itemDiv.replace(
`<div class='iqui'>${item.q}</div>`,
`<div class='iqui'>${Math.floor(item.q / 1000)}k</div>`,
);
}
itemDiv = itemDiv.replace(
/^<div /,
`<div title="${niceItemName(item)} (${item.name})" `,
);
html += itemDiv;
});
html += "</div></div>";
});
html += "<div style='clear:both;'></div></div>";
parent.show_modal(html, {
wrap: false,
hideinbackground: false,
url: "/docs/guide/all/items",
});
}
export function renderBankItemsOverview() {
const bank = character.bank ?? bankCache;
if (!bank) {
return game_log("No bank");
}
function item_p_cmp(a, b) {
if (!a.p && !b.p) {
return 0;
}
if (a.p && !b.p) {
return -1;
}
if (!a.p && b.p) {
return 1;
}
return a.p < b.p ? -1 : +(a.p > b.p);
}
function itm_cmp(a, b) {
return (
(a == null) - (b == null) ||
(a && (a.name < b.name ? -1 : +(a.name > b.name))) ||
(a && b.level - a.level) ||
(a && item_p_cmp(a, b))
);
}
const itemsByCategory = [
["Helmets", []],
["Armors", []],
["Underarmors", []],
["Gloves", []],
["Shoes", []],
["Capes", []],
["Rings", []],
["Earrings", []],
["Amulets", []],
["Belts", []],
["Orbs", []],
["Weapons", []],
["Shields", []],
["Offhands", []],
["Elixirs", []],
["Potions", []],
["Scrolls", []],
["Crafting and Collecting", []],
["Exchangeables", []],
["Others", []],
];
const slot_ids = [
"helmet",
"chest",
"pants",
"gloves",
"shoes",
"cape",
"ring",
"earring",
"amulet",
"belt",
"orb",
"weapon",
"shield",
"offhand",
"elixir",
"pot",
"scroll",
"material",
"exchange",
"",
];
object_sort(G.items, "gold_value").forEach(([, gItem]) => {
if (!gItem.ignore) {
for (let c = 0; c < itemsByCategory.length; c++) {
if (
!slot_ids[c] ||
gItem.type === slot_ids[c] ||
(slot_ids[c] === "offhand" &&
["source", "quiver", "misc_offhand"].includes(gItem.type)) ||
(slot_ids[c] === "scroll" &&
["cscroll", "uscroll", "pscroll", "offering"].includes(gItem.type)) ||
(slot_ids[c] === "exchange" && gItem.e)
) {
const dest_type = gItem.id;
const type_in_bank = [];
for (const bank_pack of objectValues(bank)) {
for (const bank_item of objectValues(bank_pack)) {
if (bank_item && bank_item.name === dest_type) {
type_in_bank.push(JSON.parse(JSON.stringify(bank_item)));
}
}
}
type_in_bank.sort(itm_cmp);
// sucessive merge, flatten
for (let io = type_in_bank.length - 1; io >= 1; io--) {
if (itm_cmp(type_in_bank[io], type_in_bank[io - 1]) === 0) {
type_in_bank[io - 1].q =
(type_in_bank[io - 1].q || 1) + (type_in_bank[io].q || 1);
type_in_bank.splice(io, 1);
}
}
itemsByCategory[c][1].push(type_in_bank);
break;
}
}
}
});
for (let c = 0; c < itemsByCategory.length; c++) {
itemsByCategory[c][1] = itemsByCategory[c][1].flat();
}
renderItemsOverview(itemsByCategory);
}
import { calculate_damage_range } from "../helpers";
import { injectIntoParent, replaceInParentFunction, wrapFunction } from "../helpers/inject";
/**
* This function will add links to selected inventory items
* to directly send items to nearby characters.
* The whole stack will be sent so be careful.
*/
function addSendItemShortcut(selector: string) {
const targets = [];
const values = Object.values(parent.entities);
for (let index = 0; index < values.length; index++) {
const entity = values[index];
if (!is_character(entity)) {
continue;
}
if (distance(character, entity) > 400) {
continue;
}
targets.push(entity);
}
targets.sort((a, b) => a.owner.localeCompare(b.owner));
if (targets.length && selector === ".inventory-item") {
for (let index = 0; index < targets.length; index++) {
const target = targets[index];
// @ts-ignore
html += parent.button_line({
name: `<span style='color:gray'>[s]</span><span style='color:white'>:</span> Send to ${target.name}`,
// @ts-ignore
onclick: `parent.code_eval(\`send_item('${target.name}', ${args.num}, 10000)\`)`,
// @ts-ignore
color: colors.gold,
});
}
}
}
function addStandInfoToComputer() {
// @ts-ignore
html +=
"<div class='clickable' onclick='socket.emit(\"trade_history\",{});' style=\"color: #44484F\">TRADE HISTORY</div>";
if (character.stand) {
// @ts-ignore
html +=
"<div class='clickable' onclick='close_merchant();' style=\"color: #8E5E2C\">CLOSE</div>";
} else {
// @ts-ignore
html += `<div class='clickable' onclick='open_merchant(locate_item("computer"));' style="color: #8E5E2C">OPEN</div>`;
}
}
const RENDER_GOLD2_MARKER = `html+=bold_prop_line("Base Gold",G.base_gold[args.monster][mname]+" <span class='gray'>("+G.maps[mname].name+")</span>","gold");
}`;
function addAnotherGoldLineForMonsters() {
// @ts-ignore
for (mname in G.base_gold[args.monster]) {
// @ts-ignore
if (!G.maps[mname] || G.maps[mname].ignore) continue;
// @ts-ignore
html += bold_prop_line(
"With goldm",
// @ts-ignore
`[${to_pretty_num(
// @ts-ignore
Math.round(G.base_gold[args.monster][mname] * character.goldm * 0.64),
)} - ${to_pretty_num(
// @ts-ignore
Math.round(G.base_gold[args.monster][mname] * character.goldm * (0.64 + 0.8)),
)}] <span class='gray'>(${
// @ts-ignore
G.maps[mname].name
})</span>`,
"gold",
);
}
}
const RENDER_DAMAGE_MARKER = ` if(item.achievements) //args.monster`;
function addDamageRange() {
// @ts-ignore
if (args.monster) {
// @ts-ignore
const dmg = inj__calculate_damage_range(item, "attack");
// @ts-ignore
html += bold_prop_line(
"Damage range",
// @ts-ignore
`${Math.floor(dmg[0])} - ${Math.floor(dmg[1])}`,
// @ts-ignore
colors.attack,
);
}
}
function addSpecialCorner() {
// @ts-ignore
if (actual && actual.p) {
let corner = "";
// @ts-ignore
switch (actual.p) {
case "festive": {
corner = `<div class='trruui imu' style='border-color: grey; color:#79ff7e'>F</div></div></div>`;
break;
}
case "firehazard": {
corner = `<div class='trruui imu' style='border-color: grey; color:#f79b11'>H</div></div></div>`;
break;
}
case "glitched": {
corner = `<div class='trruui imu' style='border-color: grey; color:grey'>#</div></div></div>`;
break;
}
case "gooped": {
corner = `<div class='trruui imu' style='border-color: grey; color:#64B867'>G</div></div></div>`;
break;
}
case "legacy": {
corner = `<div class='trruui imu' style='border-color: grey; color:white''>L</div></div></div>`;
break;
}
case "lucky": {
corner = `<div class='trruui imu' style='border-color: grey; color:#00f3ff''>L</div></div></div>`;
break;
}
case "shiny": {
corner = `<div class='trruui imu' style='border-color: grey; color:#99b2d8''>S</div></div></div>`;
break;
}
case "superfast": {
corner = `<div class='trruui imu' style='border-color: grey; color:#c681dc''>U</div></div></div>`;
break;
}
default: {
corner = `<div class='trruui imu' style='border-color: black; color:grey''>?</div></div></div>`;
}
}
// @ts-ignore
html = html.replace(/<\/div><\/div>$/, corner);
}
}
function extendDropUi() {
const TO_REPLACE = ` if(def[0]*mult>=1) html+="<div style='vertical-align: middle; display: inline-block; font-size: 24px; line-height: 50px; height: 50px; margin-left: 5px; margin-right: 8px'>"+to_pretty_float(def[0]*mult)+" / 1</div>";
else if(1/(def[0]*mult)>=1.1 && 1/(def[0]*mult)<10 && parseInt(1/(def[0]*mult))*10!=parseInt(10/(def[0]*mult))) html+="<div style='vertical-align: middle; display: inline-block; font-size: 24px; line-height: 50px; height: 50px; margin-left: 5px; margin-right: 8px'>10 / "+to_pretty_num(round(10/(def[0]*mult)))+"</div>";
else html+="<div style='vertical-align: middle; display: inline-block; font-size: 24px; line-height: 50px; height: 50px; margin-left: 5px; margin-right: 8px'>1 / "+(((1/(def[0]*mult))>=2)&&to_pretty_num(round(1/(def[0]*mult)))||to_pretty_float(1/(def[0]*mult)))+"</div>";`;
const REPLACEMENT = wrapFunction(() => {
const DISPLAY_HUMAN_PERCENT = false;
const DISPLAY_LUCKM_PROBA = true;
const __telo_proba = def[0] * mult;
const __telo_proba_percent = Math.min(Math.round(__telo_proba * 10000000) / 100000, 100);
const __telo_proba_luckm = Math.min(__telo_proba * character.luckm, 1);
const __telo_proba_luckm_percent = Math.min(
Math.round(__telo_proba_luckm * 10000) / 100,
100,
);
if (__telo_proba >= 1) {
html += `<div style="vertical-align: middle; display: inline-block; font-size: 24px; line-height: 50px; height: 50px; margin-left: 5px; margin-right: 8px">`;
html += `<div style="position: relative; white-space: nowrap; display: inline-block;">`;
html += `<span>${to_pretty_float(__telo_proba)} / 1</span>`;
if (DISPLAY_LUCKM_PROBA) {
html += `<span title="With ${
character.luckm
} luckm" style="margin-left: 10px; color: #49ff3e;">(${to_pretty_float(
__telo_proba_luckm,
)} / 1)</span>`;
}
html += `</div></div>`;
} else {
html += `<div style="vertical-align: middle; display: inline-block; font-size: 24px; line-height: 50px; height: 50px; margin-left: 5px; margin-right: 8px">`;
html += `<div style="position: relative; white-space: nowrap; display: inline-block;">`;
html += `<span>1 / ${
(1 / __telo_proba >= 2 && to_pretty_num(round(1 / __telo_proba))) ||
to_pretty_float(1 / __telo_proba)
}</span>`;
if (DISPLAY_HUMAN_PERCENT && __telo_proba_percent > 0) {
html += `<div style="z-index: 1;position: absolute;top: 1px;left: 0;height: 16px;font-size: 22px;line-height: 13px;display: inline-block;">${__telo_proba_percent}%</div>`;
}
if (DISPLAY_LUCKM_PROBA) {
html += `<span title="With ${
character.luckm
} luckm" style="margin-left: 10px; color: #49ff3e;">(1 / ${
(1 / __telo_proba_luckm >= 2 && to_pretty_num(round(1 / __telo_proba_luckm))) ||
to_pretty_float(1 / __telo_proba_luckm)
})</span>`;
}
html += `</div></div>`;
}
});
replaceInParentFunction("render_drop", TO_REPLACE, REPLACEMENT);
}
function extendTargetTopLeft() {
const RENDER_CHARACTER_MARKER = ` render_character(ctarget);`;
const REPLACEMENT = `${RENDER_CHARACTER_MARKER}render_monster(otarget, true);`;
replaceInParentFunction("reset_topleft", RENDER_CHARACTER_MARKER, REPLACEMENT);
}
/**
* Entrypoint to enable UI extensions.
*/
export function extendUi() {
// ------ render_item
const SEND_ITEM_MARKER =
' if(args.trade && actual && character.slots.helmet && character.slots.helmet.name.startsWith("ghat"))';
injectIntoParent("render_item", SEND_ITEM_MARKER, addSendItemShortcut);
// Everything that should be a stand
replaceInParentFunction("render_item", `item.type=="stand"`, `item.stand`);
// ------ render_computer
const RENDER_COMPUTER_STAND_MARKER = " $element.html(html);";
injectIntoParent("render_computer", RENDER_COMPUTER_STAND_MARKER, addStandInfoToComputer);
// ------ render_character_sheet add bottom margin
replaceInParentFunction(
// @ts-ignore
"render_character_sheet",
`var html="<div style='background-color: black;`,
`var html="<div style='margin-bottom: 50px; background-color: black;`,
);
injectIntoParent("render_item", RENDER_GOLD2_MARKER, addAnotherGoldLineForMonsters, true);
injectIntoParent("render_item", RENDER_DAMAGE_MARKER, addDamageRange, false, {
inj__calculate_damage_range: calculate_damage_range,
});
// Cleaner number format for gold
replaceInParentFunction(
"render_item",
`"Base Gold",G.base_gold[args.monster][mname]+"`,
`"Base Gold",to_pretty_num(G.base_gold[args.monster][mname])+"`,
);
// Let css do its thing with width.
replaceInParentFunction("render_item", `max-width: 240px;`, `max-width: 326px;`);
replaceInParentFunction("render_monster_info", `wwidth:240,`, `wwidth:326,`);
// Upgrade and compounds are now right above the inventory
replaceInParentFunction(
"render_upgrade_shrine",
`$("#topleftcornerui").html(html);`,
`document.querySelector('#bottomleftcorner').insertAdjacentHTML('afterbegin', html);`,
);
replaceInParentFunction(
"render_compound_shrine",
`$("#topleftcornerui").html(html);`,
`document.querySelector('#bottomleftcorner').insertAdjacentHTML('afterbegin', html);`,
);
// Add corner with icon for special items
const ITEM_ADD_SPECIAL_MARKER = " return html;";
injectIntoParent("item_container", ITEM_ADD_SPECIAL_MARKER, addSpecialCorner);
// extendTargetTopLeft();
extendDropUi();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment