Skip to content

Instantly share code, notes, and snippets.

@CyberShadow
Last active October 16, 2020 21:08
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 CyberShadow/84ab94a73ee74182e23af887c7a45860 to your computer and use it in GitHub Desktop.
Save CyberShadow/84ab94a73ee74182e23af887c7a45860 to your computer and use it in GitHub Desktop.
Terraria pickaxe comparison chart generator
/gen
/cache/
/pickaxes.svg
/*.html

HP:50 Needs:0

https://terraria.gamepedia.com/Dirt_BlockDirt Blockhttps://gamepedia.cursecdn.com/terraria_gamepedia/5/55/Dirt_Block.png?version=1e5ca391426bb74f6e159ece450ed8f91616
https://terraria.gamepedia.com/Sand_BlockSand Blockhttps://gamepedia.cursecdn.com/terraria_gamepedia/d/dc/Sand_Block.png?version=05a6b3eaa5a834173c35d8626a3911e41616
https://terraria.gamepedia.com/Clay_BlockClay Blockhttps://gamepedia.cursecdn.com/terraria_gamepedia/b/b1/Clay_Block.png?version=81dd9a1445095af1488a187148539e001616
https://terraria.gamepedia.com/Mud_BlockMud Blockhttps://gamepedia.cursecdn.com/terraria_gamepedia/4/44/Mud_Block.png?version=d3dbc8685c5dc3132cc7c6c8a4c8069c1616
https://terraria.gamepedia.com/Silt_BlockSilt Blockhttps://gamepedia.cursecdn.com/terraria_gamepedia/d/dd/Silt_Block.png?version=87ea0e2d80827e6ef6fc4d936b06b6581616
https://terraria.gamepedia.com/Ash_BlockAsh Blockhttps://gamepedia.cursecdn.com/terraria_gamepedia/4/44/Ash_Block.png?version=37baa69a8f0e0498621ccd74e9578f7b1616
https://terraria.gamepedia.com/Snow_BlockSnow Blockhttps://gamepedia.cursecdn.com/terraria_gamepedia/f/fa/Snow_Block.png?version=530e66c695623d8b208c67708baa6ea71616
https://terraria.gamepedia.com/Slush_BlockSlush Blockhttps://gamepedia.cursecdn.com/terraria_gamepedia/0/09/Slush_Block.png?version=81f87e1a0ae850d83a88552532f6b6731616
https://terraria.gamepedia.com/Hardened_Sand_BlockHardened Sand Blockhttps://gamepedia.cursecdn.com/terraria_gamepedia/7/7a/Hardened_Sand_Block.png?version=b0f5b56fac64a826a593dd018fba6f491616

HP:100 Needs:0

https://terraria.gamepedia.com/Stone_BlockStone Blockhttps://gamepedia.cursecdn.com/terraria_gamepedia/3/37/Stone_Block.png?version=87493d0bef7db103283f0a1b8f6c65201616
https://terraria.gamepedia.com/Ebonsand_BlockEbonsand Blockhttps://gamepedia.cursecdn.com/terraria_gamepedia/b/b6/Ebonsand_Block.png?version=f7dc5ca07679a73696192123720c62301616
https://terraria.gamepedia.com/Gold_OreGold Orehttps://gamepedia.cursecdn.com/terraria_gamepedia/f/f7/Gold_Ore.png?version=86619cfc58e5c713cc62366e29d742b31616
https://terraria.gamepedia.com/Gray_BrickGray Brickhttps://gamepedia.cursecdn.com/terraria_gamepedia/e/ef/Gray_Brick.png?version=8d4d6359c6ed95540cd9afec744ed5b21616

HP:100 Needs:50

https://terraria.gamepedia.com/MeteoriteMeteoritehttps://gamepedia.cursecdn.com/terraria_gamepedia/9/9e/Meteorite.png?version=c7a036b4ecd256709b391721e7aa25c01616

HP:100 Needs:55

https://terraria.gamepedia.com/Demonite_OreDemonite Orehttps://gamepedia.cursecdn.com/terraria_gamepedia/a/a8/Demonite_Ore.png?version=da4b15b0f58ad16fa8e8c551d75d5e181616
https://terraria.gamepedia.com/Crimtane_OreCrimtane Orehttps://gamepedia.cursecdn.com/terraria_gamepedia/c/c5/Crimtane_Ore.png?version=8979d0772a1521637ab05b97116fb1b71616

HP:100 Needs:65

https://terraria.gamepedia.com/ObsidianObsidianhttps://gamepedia.cursecdn.com/terraria_gamepedia/2/23/Obsidian.png?version=0b8b04d56a95bc6ca50adc0f6c81dfd41616
https://terraria.gamepedia.com/Desert_FossilDesert Fossilhttps://gamepedia.cursecdn.com/terraria_gamepedia/f/f7/Desert_Fossil.png?version=fcdbd414b59fde29fa3dc90468d48e941616

HP:200 Needs:65

https://terraria.gamepedia.com/Ebonstone_BlockEbonstone Blockhttps://gamepedia.cursecdn.com/terraria_gamepedia/6/64/Ebonstone_Block.png?version=5793e23dcce7fd12e40cabc648f21a6d1616
https://terraria.gamepedia.com/Pearlstone_BlockPearlstone Blockhttps://gamepedia.cursecdn.com/terraria_gamepedia/6/67/Pearlstone_Block.png?version=d0827351fc8bd504c1e461feecc561fa1616
https://terraria.gamepedia.com/HellstoneHellstonehttps://gamepedia.cursecdn.com/terraria_gamepedia/8/8f/Hellstone.png?version=203269e0ffb56e3b7b8c7872e1cf97e81616
https://terraria.gamepedia.com/Crimstone_BlockCrimstone Blockhttps://gamepedia.cursecdn.com/terraria_gamepedia/b/b0/Crimstone_Block.png?version=314c346bced1d81978a5cca379d12e0f1616

HP:200 Needs:100

https://terraria.gamepedia.com/Cobalt_OreCobalt Orehttps://gamepedia.cursecdn.com/terraria_gamepedia/d/d9/Cobalt_Ore.png?version=09ffedd0032109cbf9972ac534a9ab901616
https://terraria.gamepedia.com/Palladium_OrePalladium Orehttps://gamepedia.cursecdn.com/terraria_gamepedia/8/83/Palladium_Ore.png?version=64ff4928febc6286ef7a86ace47834861414

HP:300 Needs:110

https://terraria.gamepedia.com/Mythril_OreMythril Orehttps://gamepedia.cursecdn.com/terraria_gamepedia/6/66/Mythril_Ore.png?version=a5a8b1d08306509d980457ffbb4fa5771616
https://terraria.gamepedia.com/Orichalcum_OreOrichalcum Orehttps://gamepedia.cursecdn.com/terraria_gamepedia/4/43/Orichalcum_Ore.png?version=4130bbf61d3a987409b55b439828609f1414

HP:400 Needs:150

https://terraria.gamepedia.com/Adamantite_OreAdamantite Orehttps://gamepedia.cursecdn.com/terraria_gamepedia/4/4f/Adamantite_Ore.png?version=da908130debef9ccbb45d2545a7385161616
https://terraria.gamepedia.com/Titanium_OreTitanium Orehttps://gamepedia.cursecdn.com/terraria_gamepedia/3/34/Titanium_Ore.png?version=acdaf7a7788223d48520cf77dea1aa5f1412

HP:400 Needs:210

https://terraria.gamepedia.com/Lihzahrd_BrickLihzahrd Brickhttps://gamepedia.cursecdn.com/terraria_gamepedia/0/08/Lihzahrd_Brick.png?version=e7b0f0d49c1f105b21a6b8f040d4d9281616

HP:500 Needs:200

https://terraria.gamepedia.com/Chlorophyte_OreChlorophyte Orehttps://gamepedia.cursecdn.com/terraria_gamepedia/5/5b/Chlorophyte_Ore.png?version=7234b9be18d5cfad8ac4c4ff141f4ba81614
import std.algorithm.comparison;
import std.algorithm.iteration;
import std.algorithm.mutation;
import std.algorithm.searching;
import std.algorithm.sorting;
import std.array;
import std.base64;
import std.conv;
import std.exception;
import std.file;
import std.json;
import std.random;
import std.range;
import std.regex;
import std.stdio;
import std.string;
import ae.utils.math;
import ae.utils.regex;
import ae.utils.xmlbuild;
import ae.sys.net;
import ae.sys.net.cachedcurl;
void main()
{
string[int] itemNames;
foreach (line; "../re/Terraria/Terraria.ID/ItemID.cs".readText.splitLines)
if (auto m = line.matchFirst(re!`^ public const short (.+) = ([-\d]+);$`))
itemNames[m[2].to!int] = m[1];
auto englishItems = "../re/Terraria/Terraria.Localization/Content.en-US.Items.json".readText.parseJSON;
struct Pickaxe
{
int id, pick, useTime, isDrill;
}
Pickaxe[] pickaxes;
foreach (line; "picks.tsv".readText.chomp.splitLines)
{
auto parts = line.split("\t");
pickaxes ~= Pickaxe(
parts[0].to!int,
parts[1].to!int,
parts[2].to!int,
parts[3].to!int,
);
}
struct Block
{
string url, name, imgUrl;
//int imgWidth, imgHeight;
}
struct BlockTier
{
int hp, powerNeeded;
bool isDefault;
Block[] blocks;
}
BlockTier[] tiers;
foreach (line; File("blocks.org").byLine)
{
if (line.findSplit("#")[0].strip.empty)
continue;
line = line.strip;
if (line.skipOver("* "))
{
auto parts = line.split();
BlockTier tier;
parts[0].skipOver("HP:").enforce();
tier.hp = parts[0].to!int;
parts[1].skipOver("Needs:").enforce();
tier.powerNeeded = parts[1].to!int;
tiers ~= tier;
}
else
if (line.skipOver("| "))
{
auto parts = line.split("|").map!strip.array;
tiers[$-1].blocks ~= Block(
parts[0].idup,
parts[1].idup,
parts[2].idup,
// parts[3].to!int,
// parts[4].to!int
);
}
else
if (line == "...")
tiers[$-1].isDefault = true;
else
throw new Exception(line.idup);
}
auto maxBlocksInTier = tiers.map!(tier => tier.blocks.length).reduce!max;
enum powerScale = 5;
enum speedScale = 50;
enum iconSize = 32;
enum padLeft = 0/*50*/;
enum padTop = 60;
enum tierHeight = 20;
enum blockPad = 2;
enum blockSize = 16;
auto minPower = 30; // pickaxes.map!(p => p.power).reduce!min;
auto maxPower = 250; // pickaxes.map!(p => p.power).reduce!max;
auto minSpeed = 1; // pickaxes.map!(p => p.speed).reduce!min;
auto maxSpeed = 60 / 3; // pickaxes.map!(p => p.speed).reduce!max;
auto svgWidth = padLeft + (maxPower - minPower) * powerScale + (maxBlocksInTier * blockSize + (maxBlocksInTier + 1) * blockPad);
auto svgHeight = padTop + (maxSpeed - minSpeed) * speedScale + (1 + tiers.length) * tierHeight;
auto svg = newXml().svg([
"xmlns" : "http://www.w3.org/2000/svg",
"xmlns:xlink" : "http://www.w3.org/1999/xlink",
"width" : "%d".format(svgWidth),
"height" : "%d".format(svgHeight),
"style" : "font-family: Verdana, sans-serif",
]);
svg.rect(["x" : "0", "y" : "0", "width" : svgWidth.text, "height" : svgHeight.text, "fill" : "white"]);
{
auto t = svg.text([
"x" : (svgWidth / 2).text,
"y" : 45.text,
"font-size" : "40",
"text-anchor" : "middle",
]);
t[] = "Terraria pickaxes, by speed/power";
}
bool[int][int] placed;
enum numLayers = 4;
auto defs = svg.defs();
auto bg = svg.g();
auto lines = svg.g();
auto layers = numLayers.iota.map!(l => svg.g()).array;
layers.reverse();
T powerX(T)(T power) { return padLeft + (power - minPower) * powerScale; }
T speedY(T)(T speed) { return padTop + (speed - minSpeed) * speedScale; }
immutable powerBounds =
chain(
[minPower, 50, 67, 100, 150, 200, 250, /*300, 400, 500*/], // modulus boundaries
[50, 55, 65, 100, 110, 150, 200, 210], // requirements to mine
).array.sort.uniq.array.idup;
//immutable speedBounds = iota(60/maxSpeed, 60/minSpeed + 1, 3).map!(hps => 60/hps).retro.array;
immutable speedBounds = minSpeed ~ [3, 4, 5, 6, 7, 8, 10, 15, 30].map!(hps => 60./hps).retro.array.idup;
foreach (pi; 0 .. cast(int)powerBounds.length - 1)
{
auto gradId = format("grad-power-%d", pi);
auto grad = defs.linearGradient([
"id" : gradId,
"x1" : "0%",
"y1" : "0%",
"x2" : "0%",
"y2" : "100%",
]);
auto hue = itpl(240, 0, pi, 0, cast(int)powerBounds.length - 2);
auto lightness = 75 - (pi) % 2 * 3;
grad.stop(["offset" : "0%", "style" : "stop-color: hsl(%s,%s%%,%s%%)".format(hue, 100, lightness)]);
grad.stop(["offset" : "100%", "style" : "stop-color: hsl(%s,%s%%,%s%%)".format(hue, 0, lightness)]);
auto x0 = powerX(powerBounds[pi]);
auto x1 = powerX(powerBounds[pi + 1]);
auto y0 = speedY(speedBounds[0]);
auto y1 = speedY(speedBounds[$-1]);
//swap(y0, y1);
bg.rect([
"x" : x0.text,
"y" : y0.text,
"width" : (x1 - x0).text,
"height" : (y1 - y0).text,
"style" : "fill:url(#%s)".format(gradId),
]);
}
foreach (pi; 0 .. powerBounds.length)
lines.line([
"x1" : (powerX(powerBounds[pi ]) + 0.5).text,
"x2" : (powerX(powerBounds[pi ]) + 0.5).text,
"y1" : (speedY(speedBounds[0 ]) + 0.5).text,
"y2" : svgHeight .text,
"stroke" : "rgba(0,0,0,0.25)",
]);
foreach (si; 1 .. speedBounds.length)
{
lines.line([
"x1" : (powerX(powerBounds[0 ]) + 0.5 ).text,
"x2" : svgWidth .text,
"y1" : (speedY(speedBounds[si ]) + 0.5 ).text,
"y2" : (speedY(speedBounds[si ]) + 0.5 ).text,
"stroke" : "rgba(0,0,0,%s)".format(si == speedBounds.length - 1 ? 1 : 0.1),
]);
auto t = svg.text([
"x" : (powerX(maxPower) + 140).text,
"y" : (speedY(speedBounds[si]) + (si == speedBounds.length - 1 ? -2 : 8)).text,
"font-size" : "24",
"text-anchor" : "end",
]);
t[] = "%s hits/s".format(60 / speedBounds[si]);
}
foreach (axis; 0 .. 2)
{
enum arrWidth = 10;
enum arrLength = 300;
enum arrCap = 50;
auto arrGrad = defs.linearGradient(["id" : "ps"[axis] ~ "arr", "x1" : "0%", "y1" : "0%", "x2" : "100%", "y2" : "0%"]);
arrGrad.stop(["offset" : "0%", "style" : "stop-color: hsl(%s,%s%%,%s%%)".format([240, 120][axis], [100, 0][axis], 60)]);
arrGrad.stop(["offset" : "100%", "style" : "stop-color: hsl(%s,%s%%,%s%%)".format([ 0, 120][axis], [100, 100][axis], 60)]);
auto arrG = svg.g(["transform" : axis == 0
? "translate(%d.5,%d.5)" .format(50, padTop + 50)
: "translate(%d.5,%d.5) rotate(-90)".format(powerX(maxPower) - 50, speedY(maxSpeed) - 50)]);
arrG.path([
"d" : format("M 0 %d h %d v %d l %d %d l %d %d v %d h %d z",
-arrWidth/2,
arrLength,
-(arrCap - arrWidth) / 2,
arrCap / 2, arrCap / 2,
-arrCap / 2, arrCap / 2,
-(arrCap - arrWidth) / 2,
-arrLength,
),
"fill" : "url(#%sarr)".format("ps"[axis]),
"stroke" : "rgba(0,0,0,0.5)",
]);
auto arrText = arrG.text([
"x" : (arrLength / 2).text,
"y" : (-25 + axis * 50).text,
"text-anchor" : "middle",
"stroke" : "none",
// "stroke-width" : "1px",
"fill" : "black",
"dy" : ".3em",
"font-size" : "32",
]);
arrText[] = ["more powerful", "faster"][axis];
}
// Hack
uint pngWidth (ubyte[] png) { return png[19]; }
uint pngHeight(ubyte[] png) { return png[23]; }
int pickOrder(ref Pickaxe p)
{
int order = p.id;
// auto name = itemNames[p.id];
if (p.isDrill)
order += 10000;
return order;
}
pickaxes.schwartzSort!pickOrder;
foreach (ref p; pickaxes)
{
auto power = p.pick;
auto speed = p.useTime;
auto x = powerX(power);
auto y = speedY(speed);
auto name = englishItems["ItemName"][itemNames[p.id]].str;
if (name.startsWith("Chlorophyte "))
x += 5 * powerScale; // Has +1 range - don't stack with Pickaxe Axe / Drax
size_t layer;
while (placed.get(x, null).get(y, false))
x += 5, y += 5, layer++;
placed[x][y] = true;
auto png = cast(ubyte[])read(format("../content/Images.out/Item_%d.png", p.id));
enum offset = 1; // put pickaxes "inside" zones, instead of on the border
x += (offset * powerScale) - pngWidth(png) / 2;
y += (offset * powerScale) - pngHeight(png) / 2;
if (layer > 0 && pngWidth(png) * 2 > pngHeight(png) * 3) // drill
y += 10;
auto url = "https://terraria.gamepedia.com/" ~ name.replace(" ", "_");
auto a = layers[layer].a(["xlink:href" : url]);
auto img = a.image([
"x" : x.text,
"y" : y.text,
"width" : pngWidth(png).text,
"height" : pngHeight(png).text,
"xlink:href" : "data:image/png;base64," ~ cast(string)Base64.encode(png),
]);
auto title = img.title();
title[] = name;
}
{
svg.rect([
"x" : 0.text,
"y" : (speedY(speedBounds[$-1]) + 1).text,
"width" : svgWidth.text, // (powerX(powerBounds[$-1])).text,
"height" : tierHeight.text,
"fill" : "white",
]);
auto t = svg.text([
"x" : (powerX(powerBounds[$-1]) / 2).text,
"y" : (speedY(speedBounds[$-1]) + 16).text,
"font-size" : "16",
"text-anchor" : "middle",
]);
t[] = "Hits needed";
svg.line([
"x1" : (powerX(powerBounds[$-1]) / 2 + 60).text,
"y1" : (speedY(speedBounds[$-1]) + tierHeight / 2 + 0.5).text,
"x2" : (powerX(powerBounds[$-1]) + (svgWidth - powerX(powerBounds[$-1])) / 2 - 50).text,
"y2" : (speedY(speedBounds[$-1]) + tierHeight / 2 + 0.5).text,
"stroke" : "black",
"stroke-dasharray" : "1 3",
]);
t = svg.text([
"x" : (powerX(powerBounds[$-1]) + (svgWidth - powerX(powerBounds[$-1])) / 2).text,
"y" : (speedY(speedBounds[$-1]) + 16).text,
"font-size" : "16",
"text-anchor" : "middle",
]);
t[] = "to mine:";
}
foreach (ti, tier; tiers)
{
auto y = speedY(speedBounds[$-1]) + (1 + ti) * tierHeight;
if (ti % 2 == 0)
bg.rect([
"x" : powerX(powerBounds[$-1]).text,
"y" : y.text,
"width" : (svgWidth - powerX(powerBounds[$-1])).text,
"height" : tierHeight.text,
"fill" : "hsl(0,0%,90%)",
]);
foreach (pi; 0 .. cast(int)powerBounds.length - 1)
{
auto p0 = powerBounds[pi];
auto p1 = powerBounds[pi + 1] - 1;
enforce((p0 >= tier.powerNeeded) == (p1 >= tier.powerNeeded), "Needed power mismatch within power bounds group");
int hits;
if (p0 < tier.powerNeeded)
hits = 0;
else
{
if (p0 != 30) // intentional mismatch
enforce((tier.hp + p0 - 1) / p0 == (tier.hp + p1 - 1) / p1, "Needed hits mismatch within power bounds group: %s (%s .. %s)".format(tier, p0, p1));
hits = (tier.hp + p1 - 1) / p1;
}
bg.rect([
"x" : powerX(powerBounds[pi]).text,
"y" : y.text,
"width" : (powerX(powerBounds[pi + 1]) - powerX(powerBounds[pi])).text,
"height" : tierHeight.text,
"fill" : "hsl(%d,100%%,%d%%)".format(
hits == 0 ? 0 : 60 + hits * 60,
[90, 85, 85, 95, 90][hits] - (ti % 2 == 0 ? 5 : 0),
),
]);
auto t = svg.text([
"x" : ((powerX(powerBounds[pi]) + powerX(powerBounds[pi + 1])) / 2).text,
"y" : (y + 16).text,
"font-size" : "16",
"font-weight" : "bold",
"text-anchor" : "middle",
"fill" : "black",
]);
t[] = hits ? text(hits) : "🚫";
}
foreach (bi, block; tier.blocks)
{
auto x = powerX(powerBounds[$-1])
+ (svgWidth - powerX(powerBounds[$-1])) / 2
- (tier.blocks.length * blockSize + (tier.blocks.length - 1) * blockPad) / 2
+ bi * (blockSize + blockPad);
auto a = svg.a(["xlink:href" : block.url]);
auto png = cast(ubyte[])getFile(block.imgUrl);
auto img = a.image([
"x" : (x + (blockSize - pngWidth(png)) / 2).text,
"y" : (y + (blockSize - pngHeight(png)) / 2 + (tierHeight - blockSize) / 2).text,
"width" : pngWidth(png).text,
"height" : pngHeight(png).text,
"xlink:href" : "data:image/png;base64," ~ cast(string)Base64.encode(png),
]);
auto title = img.title();
title[] = block.name;
}
}
svg.toPrettyString.toFile("pickaxes.svg");
}
1 40 13 0
103 65 15 0
122 100 18 0
385 110 7 1
386 150 6 1
388 180 4 1
579 200 4 1
776 110 13 0
777 150 10 0
778 180 8 0
798 70 14 0
882 35 16 0
990 200 7 0
1188 130 12 0
1189 130 7 1
1195 165 9 0
1196 165 5 1
1202 190 7 0
1203 190 4 1
1230 200 7 0
1231 200 4 1
1294 210 6 0
1320 55 11 0
1506 200 10 0
1917 55 16 0
2176 200 4 0
2341 59 13 0
2774 225 2 1
2776 225 6 0
2779 225 2 1
2781 225 6 0
2784 225 2 1
2786 225 6 0
2798 230 6 0
3464 225 2 1
3466 225 6 0
3485 59 15 0
3491 50 19 0
3497 43 12 0
3503 35 14 0
3509 35 15 0
3515 45 11 0
3521 55 17 0
4059 55 14 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment