|
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"); |
|
} |