Thu Sep 26 2024
Bitburner is a complex game that combines hacking, scripting, and automation. To enhance my gameplay, I created a custom user interface (UI) that visualizes real-time game data. This UI helps track hacking progress, resources, and target nodes, making the gameplay smoother and more strategic.
In addition to this, I experimented with building a custom React component renderer and a diffing algorithm tailored for Bitburner. The unique challenge was that Bitburner doesn’t easily allow long-living scripts, so using traditional React libraries like react-dom
wasn't feasible.
The UI functions as an interactive game HUD, tapping into Bitburner's API to display live stats, targets, and node performance in a responsive layout. The core features include:
Additionally, I built a custom React rendering engine from scratch to work within Bitburner's limitations. This allows for dynamic UI updates using familiar React-style components, without relying on traditional libraries like react-dom
. I also implemented a diffing and patching algorithm to efficiently update the DOM without restarting the script, which is crucial in Bitburner's environment where scripts are usually short-lived.
The User
component displays the player's hacking stats, including their hacking level, available funds, and money-per-second rate.
const User = ({ ns }) => {
const hackLvl = ns.getHackingLevel();
const money = ns.getServerMoneyAvailable('home');
return React.createElement(
CollapsibleSection,
{ id: 'user-section' },
Row({ children: ["Hck Lvl:", hackLvl] }),
Row({ children: ["Funds:", formatNumber(money)] }),
Row({ children: ["Run $/s:", formatNumber(calculateMoneyPerSecond())] })
);
};
The Target
component monitors attack targets, displaying server money available and security level with visual progress bars.
const Target = ({ ns, target }) => {
const money = ns.getServerMoneyAvailable(target.name);
const security = ns.getServerSecurityLevel(target.name);
return React.createElement(
'div',
{ style: {} },
Row({ children: [target.name, "Money:", formatNumber(money)] }),
Row({ children: ["Security:", formatNumber(security)] })
);
};
The Node
component tracks hacking operations on each active node, using ProgressBar
components to visualize hack/grow/weaken progress.
const Node = ({ ns, node, stats }) => {
const maxRam = ns.getServerMaxRam(node);
return React.createElement(
'div',
{ style: {} },
Row({ children: [node, "Max RAM:", formatNumber(maxRam)] }),
ProgressBar({ percent: stats.hack / maxRam, label: "Hack" }),
ProgressBar({ percent: stats.grow / maxRam, label: "Grow" }),
ProgressBar({ percent: stats.weaken / maxRam, label: "Weaken" })
);
};
One of the main challenges I faced was working around Bitburner’s limitations, where long-living scripts are discouraged. This meant I couldn’t rely on React’s react-dom
for efficient DOM updates. Instead, I decided to build a custom renderer that mimics React's component system while integrating directly with the game’s DOM.
The custom renderer takes in React components, processes them recursively, and creates HTML elements that are manually appended to the DOM. Here's an example of how the rendering works:
export function renderToHTMLElement(element) {
if (typeof element === 'string' || typeof element === 'number') {
return document.createTextNode(element);
}
if (typeof element.type === 'function') {
const renderedElement = element.type(element.props);
return renderToHTMLElement(renderedElement);
}
const domNode = document.createElement(element.type);
Object.entries(element.props || {}).forEach(([key, value]) => {
if (key === 'children') return;
if (/^on[A-Z]/.test(key)) {
domNode.addEventListener(key.toLowerCase().substring(2), value);
} else if (key === 'style') {
Object.assign(domNode.style, value);
} else {
domNode.setAttribute(key, value);
}
});
const children = Array.isArray(element.props?.children)
? element.props.children
: [element.props?.children || ''];
children.forEach(child => domNode.appendChild(renderToHTMLElement(child)));
return domNode;
}
To optimize performance, I implemented a diffing algorithm that compares the old and new virtual DOM trees and selectively applies changes to the real DOM. This keeps the UI responsive while minimizing unnecessary updates.
function diffAndUpdate(oldNode, newNode) {
if (!newNode) {
oldNode.remove();
return;
}
if (oldNode.nodeType !== newNode.nodeType || oldNode.nodeName !== newNode.nodeName) {
oldNode.replaceWith(newNode);
return;
}
if (oldNode.nodeType === Node.TEXT_NODE && oldNode.textContent !== newNode.textContent) {
oldNode.textContent = newNode.textContent;
return;
}
const oldAttrs = Array.from(oldNode.attributes);
const newAttrs = Array.from(newNode.attributes);
oldAttrs.forEach(attr => {
if (!newNode hasAttribute(attr.name)) {
oldNode.removeAttribute(attr.name);
}
});
newAttrs.forEach(attr => {
if (oldNode.getAttribute(attr.name) !== attr.value) {
oldNode.setAttribute(attr.name, attr.value);
}
});
const oldChildren = Array.from(oldNode.childNodes);
const newChildren = Array.from(newNode.childNodes);
const maxLength = Math.max(oldChildren.length, newChildren.length);
for (let i = 0;i < maxLength;i++) {
diffAndUpdate(oldChildren[i], newChildren[i]);
}
}
The UI features a minimalist, dark theme to match Bitburner’s aesthetic, with vibrant progress indicators. Styling is centralized in a theme.js
file:
const theme = {
color: {
blue: '#4a90e2',
yellow: '#f4d03f',
green: '#27ae60',
red: '#e74c3c',
white: '#ecf0f1',
},
size: {
font: {
md: '14px',
},
},
spacing: {
sm: '8px',
md: '16px',
lg: '24px',
},
};
This custom UI, along with the custom React renderer and diffing system, has significantly improved my gameplay by providing real-time data tracking and resource management. Building a custom rendering engine was an exciting challenge that opened up possibilities for more tailored UIs in Bitburner.
I'm continuing to refine the interface with more features like notifications and enhanced visuals. This project showcases how a well-designed UI and custom rendering logic can elevate both gameplay and strategy in Bitburner.