Tech tree rendering + layout

This commit is contained in:
Anuken 2019-01-19 00:26:39 -05:00
parent 4ff1d0c2e1
commit 49253964d8
11 changed files with 1036 additions and 549 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 B

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 617 KiB

After

Width:  |  Height:  |  Size: 614 KiB

View File

@ -0,0 +1,56 @@
package io.anuke.mindustry.content;
import io.anuke.mindustry.game.Content;
import io.anuke.mindustry.game.ContentList;
import io.anuke.mindustry.type.ItemStack;
import static io.anuke.mindustry.type.ItemStack.with;
public class TechTree implements ContentList{
public static TechNode root;
@Override
public void load(){
root = new TechNode(Items.copper, with(),
new TechNode(Blocks.copperWall, with(Items.copper, 100),
new TechNode(Blocks.copperWallLarge, with(Items.copper, 100))
),
new TechNode(Blocks.conveyor, with(Items.copper, 10),
new TechNode(Blocks.router, with(Items.copper, 10),
new TechNode(Blocks.distributor, with(Items.copper, 10)),
new TechNode(Blocks.overflowGate, with(Items.copper, 10)),
new TechNode(Blocks.sorter, with(Items.copper, 10))
),
new TechNode(Blocks.junction, with(Items.copper, 10)),
new TechNode(Blocks.itemBridge, with(Items.copper, 10))
),
new TechNode(Items.lead, with(),
new TechNode(Items.metaglass, with(),
new TechNode(Blocks.conduit, with(Items.metaglass, 10)),
new TechNode(Blocks.liquidJunction, with(Items.metaglass, 10)),
new TechNode(Blocks.liquidRouter, with(Items.metaglass, 10),
new TechNode(Blocks.liquidTank, with(Items.metaglass, 10))
)
)
)
);
}
public static class TechNode{
public final Content content;
public final ItemStack[] requirements;
public final TechNode[] children;
public TechNode parent;
TechNode(Content content, ItemStack[] requirements, TechNode... children){
this.content = content;
this.requirements = requirements;
this.children = children;
for(TechNode node : children){
node.parent = this;
}
}
}
}

View File

@ -45,6 +45,7 @@ public class ContentLoader{
new UnitTypes(),
new Blocks(),
new Recipes(),
new TechTree(),
new Zones(),
//these are not really content classes, but this makes initialization easier

View File

@ -67,6 +67,7 @@ public class UI implements ApplicationListener{
public UnlocksDialog unlocks;
public ContentInfoDialog content;
public DeployDialog deploy;
public TechTreeDialog tech;
public Cursor drillCursor, unloadCursor;
@ -177,6 +178,7 @@ public class UI implements ApplicationListener{
localplayers = new LocalPlayerDialog();
content = new ContentInfoDialog();
deploy = new DeployDialog();
tech = new TechTreeDialog();
Group group = Core.scene.root;

View File

@ -301,11 +301,6 @@ public class DesktopInput extends InputHandler{
return !controlling ? Core.input.mouseY() : controly;
}
@Override
public boolean isCursorVisible(){
return controlling;
}
@Override
public void updateController(){

View File

@ -0,0 +1,310 @@
package io.anuke.mindustry.ui;
import io.anuke.arc.collection.FloatArray;
import io.anuke.arc.math.geom.Rectangle;
/**
* Algorithm taken from <a href="https://github.com/abego/treelayout">TreeLayout</a>.
*/
public class TreeLayout{
public TreeLocation rootLocation = TreeLocation.top;
public TreeAlignment alignment = TreeAlignment.awayFromRoot;
public float gapBetweenLevels = 10;
public float gapBetweenNodes = 10f;
private final FloatArray sizeOfLevel = new FloatArray();
private float boundsLeft = Float.MAX_VALUE;
private float boundsRight = Float.MIN_VALUE;
private float boundsTop = Float.MAX_VALUE;
private float boundsBottom = Float.MIN_VALUE;
public void layout(TreeNode root){
firstWalk(root, null);
calcSizeOfLevels(root, 0);
secondWalk(root, -root.prelim, 0, 0);
}
private float getWidthOrHeightOfNode(TreeNode treeNode, boolean returnWidth){
return returnWidth ? treeNode.width : treeNode.height;
}
private float getNodeThickness(TreeNode treeNode){
return getWidthOrHeightOfNode(treeNode, !isLevelChangeInYAxis());
}
private float getNodeSize(TreeNode treeNode){
return getWidthOrHeightOfNode(treeNode, isLevelChangeInYAxis());
}
private boolean isLevelChangeInYAxis(){
return rootLocation == TreeLocation.top || rootLocation == TreeLocation.bottom;
}
private int getLevelChangeSign(){
return rootLocation == TreeLocation.bottom || rootLocation == TreeLocation.right ? -1 : 1;
}
private void updateBounds(TreeNode node, float centerX, float centerY){
float width = node.width;
float height = node.height;
float left = centerX - width / 2;
float right = centerX + width / 2;
float top = centerY - height / 2;
float bottom = centerY + height / 2;
if(boundsLeft > left){
boundsLeft = left;
}
if(boundsRight < right){
boundsRight = right;
}
if(boundsTop > top){
boundsTop = top;
}
if(boundsBottom < bottom){
boundsBottom = bottom;
}
}
public Rectangle getBounds(){
return new Rectangle(0, 0, boundsRight - boundsLeft, boundsBottom - boundsTop);
}
private void calcSizeOfLevels(TreeNode node, int level){
float oldSize;
if(sizeOfLevel.size <= level){
sizeOfLevel.add(0);
oldSize = 0;
}else{
oldSize = sizeOfLevel.get(level);
}
float size = getNodeThickness(node);
if(oldSize < size){
sizeOfLevel.set(level, size);
}
if(!node.isLeaf()){
for(TreeNode child : node.children){
calcSizeOfLevels(child, level + 1);
}
}
}
public int getLevelCount(){
return sizeOfLevel.size;
}
public float getGapBetweenNodes(TreeNode a, TreeNode b){
return gapBetweenNodes;
}
public float getSizeOfLevel(int level){
if(!(level >= 0)) throw new IllegalArgumentException("level must be >= 0");
if(!(level < getLevelCount())) throw new IllegalArgumentException("level must be < levelCount");
return sizeOfLevel.get(level);
}
private TreeNode getAncestor(TreeNode node){
return node.ancestor != null ? node.ancestor : node;
}
private float getDistance(TreeNode v, TreeNode w){
float sizeOfNodes = getNodeSize(v) + getNodeSize(w);
return sizeOfNodes / 2 + getGapBetweenNodes(v, w);
}
private TreeNode nextLeft(TreeNode v){
return v.isLeaf() ? v.thread : v.children[0];
}
private TreeNode nextRight(TreeNode v){
return v.isLeaf() ? v.thread : v.children[v.children.length - 1];
}
private int getNumber(TreeNode node, TreeNode parentNode){
if(node.number == -1){
int number = 1;
for(TreeNode child : parentNode.children){
child.number = number++;
}
}
return node.number;
}
private TreeNode ancestor(TreeNode vIMinus, TreeNode parentOfV, TreeNode defaultAncestor){
TreeNode ancestor = getAncestor(vIMinus);
return ancestor.parent == parentOfV ? ancestor : defaultAncestor;
}
private void moveSubtree(TreeNode wMinus, TreeNode wPlus, TreeNode parent, float shift){
int subtrees = getNumber(wPlus, parent) - getNumber(wMinus, parent);
wPlus.change = wPlus.change - shift / subtrees;
wPlus.shift = wPlus.shift + shift;
wMinus.change = wMinus.change + shift / subtrees;
wPlus.prelim = wPlus.prelim + shift;
wPlus.mode = wPlus.mode + shift;
}
private TreeNode apportion(TreeNode v, TreeNode defaultAncestor,
TreeNode leftSibling, TreeNode parentOfV){
if(leftSibling == null){
return defaultAncestor;
}
TreeNode vOPlus = v;
TreeNode vIPlus = v;
TreeNode vIMinus = leftSibling;
TreeNode vOMinus = parentOfV.children[0];
float sIPlus = (vIPlus).mode;
float sOPlus = (vOPlus).mode;
float sIMinus = (vIMinus).mode;
float sOMinus = (vOMinus).mode;
TreeNode nextRightVIMinus = nextRight(vIMinus);
TreeNode nextLeftVIPlus = nextLeft(vIPlus);
while(nextRightVIMinus != null && nextLeftVIPlus != null){
vIMinus = nextRightVIMinus;
vIPlus = nextLeftVIPlus;
vOMinus = nextLeft(vOMinus);
vOPlus = nextRight(vOPlus);
vOPlus.ancestor = v;
float shift = (vIMinus.prelim + sIMinus)
- (vIPlus.prelim + sIPlus)
+ getDistance(vIMinus, vIPlus);
if(shift > 0){
moveSubtree(ancestor(vIMinus, parentOfV, defaultAncestor),
v, parentOfV, shift);
sIPlus = sIPlus + shift;
sOPlus = sOPlus + shift;
}
sIMinus += vIMinus.mode;
sIPlus += vIPlus.mode;
sOMinus += vOMinus.mode;
sOPlus += vOPlus.mode;
nextRightVIMinus = nextRight(vIMinus);
nextLeftVIPlus = nextLeft(vIPlus);
}
if(nextRightVIMinus != null && nextRight(vOPlus) == null){
vOPlus.thread = nextRightVIMinus;
vOPlus.mode += sIMinus - sOPlus;
}
if(nextLeftVIPlus != null && nextLeft(vOMinus) == null){
vOMinus.thread = nextLeftVIPlus;
vOMinus.mode += sIPlus - sOMinus;
defaultAncestor = v;
}
return defaultAncestor;
}
private void executeShifts(TreeNode v){
float shift = 0;
float change = 0;
for(int i = v.children.length - 1; i >= 0; i --){
TreeNode w = v.children[i];
change = change + w.change;
w.prelim += shift;
w.mode += shift;
shift += w.shift + change;
}
}
private void firstWalk(TreeNode v, TreeNode leftSibling){
if(v.isLeaf()){
if(leftSibling != null){
v.prelim = leftSibling.prelim + getDistance(v, leftSibling);
}
}else{
TreeNode defaultAncestor = v.children[0];
TreeNode previousChild = null;
for(TreeNode w : v.children){
firstWalk(w, previousChild);
defaultAncestor = apportion(w, defaultAncestor, previousChild,
v);
previousChild = w;
}
executeShifts(v);
float midpoint = (v.children[0].prelim + v.children[v.children.length-1].prelim) / 2f;
TreeNode w = leftSibling;
if(w != null){
v.prelim = w.prelim + getDistance(v, w);
v.mode = v.prelim - midpoint;
}else{
v.prelim = midpoint;
}
}
}
private void secondWalk(TreeNode v, float m, int level, float levelStart){
float levelChangeSign = getLevelChangeSign();
boolean levelChangeOnYAxis = isLevelChangeInYAxis();
float levelSize = getSizeOfLevel(level);
float x = v.prelim + m;
float y;
if(alignment == TreeAlignment.center){
y = levelStart + levelChangeSign * (levelSize / 2);
}else if(alignment == TreeAlignment.towardsRoot){
y = levelStart + levelChangeSign * (getNodeThickness(v) / 2);
}else{
y = levelStart + levelSize - levelChangeSign
* (getNodeThickness(v) / 2);
}
if(!levelChangeOnYAxis){
float t = x;
x = y;
y = t;
}
v.x = x;
v.y = y;
updateBounds(v, x, y);
if(!v.isLeaf()){
float nextLevelStart = levelStart
+ (levelSize + gapBetweenLevels)
* levelChangeSign;
for(TreeNode w : v.children){
secondWalk(w, m + v.mode, level + 1, nextLevelStart);
}
}
}
public enum TreeLocation{
top, left, bottom, right
}
public enum TreeAlignment{
center, towardsRoot, awayFromRoot
}
public static class TreeNode{
public float width, height, x, y;
//should be initialized by user
public TreeNode[] children;
public TreeNode parent;
private float mode, prelim, change, shift;
private int number = -1;
private TreeNode thread, ancestor;
boolean isLeaf(){
return children == null || children.length == 0;
}
}
}

View File

@ -26,6 +26,7 @@ public class DeployDialog extends FloatingDialog{
cont.clear();
addCloseButton();
buttons.addButton("$techtree", () -> ui.tech.show()).size(200f, 60f);
cont.stack(new Table(){{
top().left().margin(10);

View File

@ -0,0 +1,107 @@
package io.anuke.mindustry.ui.dialogs;
import io.anuke.arc.Core;
import io.anuke.arc.collection.ObjectSet;
import io.anuke.arc.graphics.g2d.Draw;
import io.anuke.arc.graphics.g2d.Lines;
import io.anuke.arc.graphics.g2d.ScissorStack;
import io.anuke.arc.graphics.g2d.TextureRegion;
import io.anuke.arc.input.KeyCode;
import io.anuke.arc.math.geom.Rectangle;
import io.anuke.arc.scene.Element;
import io.anuke.arc.scene.event.InputEvent;
import io.anuke.arc.scene.event.InputListener;
import io.anuke.mindustry.content.TechTree;
import io.anuke.mindustry.content.TechTree.TechNode;
import io.anuke.mindustry.game.Content;
import io.anuke.mindustry.game.UnlockableContent;
import io.anuke.mindustry.graphics.Palette;
import io.anuke.mindustry.ui.TreeLayout;
import io.anuke.mindustry.ui.TreeLayout.TreeNode;
import io.anuke.mindustry.world.Block;
public class TechTreeDialog extends FloatingDialog{
private TreeLayout layout;
private ObjectSet<TechTreeNode> nodes = new ObjectSet<>();
public TechTreeDialog(){
super("$techtree");
layout = new TreeLayout();
layout.gapBetweenLevels = 60f;
layout.gapBetweenNodes = 40f;
layout.layout(new TechTreeNode(TechTree.root, null));
cont.add(new View()).grow();
addCloseButton();
}
class TechTreeNode extends TreeNode{
final TechNode node;
public TechTreeNode(TechNode node, TreeNode parent){
this.node = node;
this.parent = parent;
this.width = this.height = 60f;
nodes.add(this);
if(node.children != null){
children = new TechTreeNode[node.children.length];
for(int i = 0; i < children.length; i++){
children[i] = new TechTreeNode(node.children[i], this);
}
}
}
}
class View extends Element{
float panX = 0, panY = 0;
Rectangle clip = new Rectangle();
{
addListener(new InputListener(){
@Override
public void touchDragged(InputEvent event, float x, float y, int pointer){
super.touchDragged(event, x, y, pointer);
panX += Core.input.deltaX(pointer);
panY += Core.input.deltaY(pointer);
}
@Override
public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){
return true;
}
});
}
@Override
public void draw(){
if(!ScissorStack.pushScissors(clip.set(x, y, width, height))){
return;
}
float offsetX = panX + width/2f + x, offsetY = panY + height/2f + y;
Lines.stroke(3f, Palette.accent);
for(TreeNode node : nodes){
for(TreeNode child : node.children){
Lines.line(node.x + offsetX, node.y + offsetY, child.x + offsetX, child.y + offsetY);
}
}
Draw.color();
for(TechTreeNode node : nodes){
Draw.drawable("content-background", node.x + offsetX - node.width/2f, node.y + offsetY - node.height/2f, node.width, node.height);
Content content = node.node.content;
TextureRegion region = content instanceof Block ? ((Block)content).getEditorIcon() :
((UnlockableContent)content).getContentIcon();
Draw.rect(region, node.x + offsetX, node.y + offsetY, 8*3, 8*3);
}
ScissorStack.popScissors();
}
}
}

View File

@ -17,7 +17,7 @@ public class LiquidHeatGenerator extends ItemLiquidGenerator{
super.setStats();
stats.remove(BlockStat.basePowerGeneration);
// Right now, Lava is the only thing that can be used.
// right now, Lava is the only thing that can be used.
stats.add(BlockStat.basePowerGeneration, powerProduction * getLiquidEfficiency(Liquids.slag) / maxLiquidGenerate * 60f, StatUnit.powerSecond);
}