There was a time when Diablo2 took many of my hours in front of the screen and with a remastered version it was just a question of time when it would catch me again. I occasionally play the Resurrected remaster and one of the main scopes of the game is searching for runes. Same way it was in the original.
For an overview on how runes in Diablo2 are upgraded, check out the visualizer at https://d2r.annoyed.dev/.
Runes are considered valuable items in the game that also act as a currency. One can use runes either to upgrade own gear or buy it from other players. There are a total of 33 runes in the game, each rune being an upgrade of the previous one. Upgrading runes involves taking some amount of similar runes and combining them into one rune of the next level.
Savvy players know how hard it is to get high level runes. One way, along farming, of doing it is transmuting runes using the horadric cube. And this approach requires lots of them, but it might not be so obvious in numbers.
If we take a crazy attempt to transmute the cheapest El rune into the last Zod rune - how many El's would it take? And how long time-wise?
Taking a little math here... Up until the 21st quality rune - Pul - player would need 3 of each type (ignore the additional gems). Up next, the last 12 levels require only two runes of each type up until Zod.
So 320*212 which gets us to roughly 14 trillions (14.281.868.906.496 to be exact) of El's!
How would it look in reality? To visualize the speed of things, I created a small rune transmutation visualizer - https://d2r.annoyed.dev/
Enough said that ever since Diablo2 was released (in 2000), and taking into assumption that each second player gets an El, we reached only to a Fal rune until now.
Making of
The idea is to have an interface that simulates how runes are stashed and transmuted. That is, each rune is represented as a list with 5 items: title, gem (if needed) and three rune slots. Depending on the rune level, there are either 3 or 2 runes shown.
Each list is considered a runeset, hence a set of 3 runes is required to transmute into a next level rune.
The page uses vanilla javascript to visualize the process.
That is, at every iteration the script adds an El rune to the runeset. If the set is full, move to the next runeset and add a rune. On each filled runeset move forward to the next runeset, if not - go back to El. Repeat.
setInterval(function () {
if (runeSetFilled(current)) {
// Whether the rune set is filled (all runes exist to transmute), reset it.
// Move forward until there are full rune sets.
while (runeSetFilled(current)) {
runeSetReset(current);
current = next(current);
// We reached Zod, which is unlikely in near future.
if (current === null) {
current = elRune;
}
}
}
// Add rune to current set.
runeSetFill(current);
// If we moved forward, get back to El rune, to start over.
if (!runeSetFilled(current)) {
current = elRune;
}
}, timeout);
An interval is defined, assigned to timeout variable. At this interval runes are generated. The default value is 1000, which is 1000 milliseconds or 1 second.
Initialize the counter numbers
The page includes rune counters in each runeset, so it is needed to initialize the numbers with numbers showing how many runes have been created so far. Starting period is defined as the game release date - 29 june 2000.
When page loads, the code takes the difference in seconds between current and initial date (including time). Since the default rune stash/transmutation speed is one second, then the difference of seconds between the dates above would be close to the amount of El runes that have been possibly stashed.
To calculate the amount of runes in each runeset, a concept of weight is introduced. Weight denotes how many stash/transmutations are required to get a rune of a given level. The weight for each rune is pre-calculated and stored as a data attribute in each runeset list. For each rune it equals the amount of El runes required, plus the number of iterations used for transmutation(s).
The numbers are as following:
Rune | Weight |
---|---|
El | 1 |
Eld | 4 |
Tir | 13 |
Nef | 40 |
Eth | 121 |
Ith | 364 |
Tal | 1093 |
Ral | 3280 |
Ort | 9841 |
Thul | 29524 |
Amn | 88573 |
Sol | 265720 |
Shael | 797161 |
Dol | 2391484 |
Hel | 7174453 |
Io | 21523360 |
Lum | 64570081 |
Ko | 193710244 |
Fal | 581130733 |
Lem | 1743392200 |
Pul | 3486784401 |
Um | 6973568803 |
Mal | 13947137607 |
Ist | 27894275215 |
Gul | 55788550431 |
Vex | 111577100863 |
Ohm | 223154201727 |
Lo | 446308403455 |
Sur | 892616806911 |
Ber | 1785233613823 |
Jah | 3570467227647 |
Cham | 7140934455295 |
Zod | 14281868910591 |
The weight of Eld rune is 4, because we need three El's and one transmutation.
The weight of Tir is 13, because we need 9 El's to get 3 Eld's and one transmutation of Eld's to get a Tir. Taking it differently, we need 4 iterations to get an Eld, eventually 3 Eld's to get a Tir. 3*4=12 and additional step to transmute 12+1=13. Same applies for all other runes. It just gets that numbers are drastically increased when moving towards higher level runes.
Whole process can be split into two steps:
- Calculate the amount of runes that are visually displayed (on/off);
- Calculate leftover amount in the counter above each runeset.
Calculate the visual amount
We start the calculations from the rune with the largest weight we could transmute so far. It's handy to make the calculation starting with the most expensive rune since this aids the calculation of all other underlying runes that would be needed to get a high level rune. Hence for a Fal we need three Ko's, 9 Lum's, etc.
For this, we lookup for a rune with a weight that is within the time difference we calculated earlier. Why so? As mentioned earlier, the weight of the rune is in fact the amount of El's, plus the transmutation steps. Our goal is, first, to calculate the visual part of the runeset and mark them accordingly. We visualize only up to three or two runes (depending on rune level). So, in theory, up to three iterations can be made. This is where weight (time cost) of the rune comes into play.
Since each consequent rune costs 3 or 2 previous runes, we can sum it's weight also only up to 3 or 2 times. Larger weight would mean that we reached a higher level rune.
Let's say that our time difference is equal to 20 seconds. The most costly rune is Tir, as its weight is 13. 20 seconds minus 13 "weight" leaves us with 7 seconds. Within those 7 seconds all that we could manage is an Eld rune, whose weight is 4. 7 seconds minus 4 equals 3, which, finally, were "wasted" on creating 3 El's, hence its weight is 1.
Calculate the total amount in counter
After we are done with the visual part, it's required to calculate the total amount of runes created so far. That said, if we are at rune Ort, but before that it calculated 6 Tal's, we must add to Ort count the result of multiplying the number of Ort's we needed to get the amount of Tal's.
function initializeRunesCount() {
let start = new Date('2000-06-29T00:00:00.000Z');
let now = new Date();
let diff = Math.round((now.getTime() - start.getTime()) / 1000);
let remainder = diff;
let runes = document.querySelectorAll('.rune__item');
// Rune "cost";
let runeWeight = 0;
// Count of previous, high level, runes.
let prevCount = 0;
// How many runes we need to get next rune.
let runeMultiplier = 1;
// Move backwards.
for (let i = runes.length - 1; i >= 0; i--) {
runeWeight = parseInt(runes[i].dataset.weight);
runeMultiplier = parseInt(runes[i].dataset.required);
// Seek further for higher weighted rune.
if (runeWeight > diff) {
continue;
}
// Rune's weight is within the time diff.
// Visually mark the runes that are left over.
while (runeWeight <= remainder) {
if (remainder > 0) {
remainder -= runeWeight;
}
runeSetMark(runes[i]);
runeSetCountSet(runes[i], runeSetCountGet(runes[i]) + 1);
}
// Now calculate their total amount, assuming next level rune count.
runeSetCountSet(runes[i], runeSetCountGet(runes[i]) + prevCount * runeMultiplier);
prevCount = runeSetCountGet(runes[i]);
}
}
The javascript implementation is plain and straightforward and does utilize any library of some sort. As it comes to vanilla javascript and dom manipulation, it's a bunch of code stacked together without any tinkering into readability of reuse. In future articles we'll try to investigate what can be done to make it more readable and robust, using what modern javascript has for us.