package mindustry.ui.fragments; import arc.*; import arc.graphics.*; import arc.input.*; import arc.math.geom.*; import arc.scene.*; import arc.scene.event.*; import arc.scene.style.*; import arc.scene.ui.*; import arc.scene.ui.layout.*; import arc.struct.*; import arc.util.*; import mindustry.content.*; import mindustry.gen.*; import mindustry.entities.units.*; import mindustry.game.EventType.*; import mindustry.graphics.*; import mindustry.input.*; import mindustry.type.*; import mindustry.ui.*; import mindustry.world.*; import static mindustry.Vars.*; public class PlacementFragment extends Fragment{ final int rowWidth = 4; public Category currentCategory = Category.distribution; Array returnArray = new Array<>(); Array returnCatArray = new Array<>(); boolean[] categoryEmpty = new boolean[Category.all.length]; ObjectMap selectedBlocks = new ObjectMap<>(); ObjectFloatMap scrollPositions = new ObjectFloatMap<>(); Block hovered, lastDisplay; Tile lastHover; Tile hoverTile; Table blockTable, toggler, topTable; ScrollPane blockPane; boolean lastGround; boolean blockSelectEnd; int blockSelectSeq; long blockSelectSeqMillis; Binding[] blockSelect = { Binding.block_select_01, Binding.block_select_02, Binding.block_select_03, Binding.block_select_04, Binding.block_select_05, Binding.block_select_06, Binding.block_select_07, Binding.block_select_08, Binding.block_select_09, Binding.block_select_10, Binding.block_select_left, Binding.block_select_right, Binding.block_select_up, Binding.block_select_down }; public PlacementFragment(){ Events.on(WorldLoadEvent.class, event -> { Core.app.post(() -> { control.input.block = null; rebuild(); }); }); Events.on(UnlockEvent.class, event -> { if(event.content instanceof Block){ rebuild(); } }); Events.on(ResetEvent.class, event -> { selectedBlocks.clear(); }); } void rebuild(){ currentCategory = Category.turret; Group group = toggler.getParent(); int index = toggler.getZIndex(); toggler.remove(); build(group); toggler.setZIndex(index); } boolean gridUpdate(InputHandler input){ scrollPositions.put(currentCategory, blockPane.getScrollY()); if(Core.input.keyDown(Binding.pick) && player.isBuilder()){ //mouse eyedropper select Tilec tile = world.entWorld(Core.input.mouseWorld().x, Core.input.mouseWorld().y); Block tryRecipe = tile == null ? null : tile.block(); for(BuildRequest req : player.builder().requests()){ if(!req.breaking && req.block.bounds(req.x, req.y, Tmp.r1).contains(Core.input.mouseWorld())){ tryRecipe = req.block; break; } } if(tryRecipe != null && tryRecipe.isVisible() && unlocked(tryRecipe)){ input.block = tryRecipe; currentCategory = input.block.category; return true; } } if(ui.chatfrag.shown() || Core.scene.hasKeyboard()) return false; for(int i = 0; i < blockSelect.length; i++){ if(Core.input.keyTap(blockSelect[i])){ if(i > 9) { //select block directionally Array blocks = getByCategory(currentCategory); Block currentBlock = getSelectedBlock(currentCategory); for(int j = 0; j < blocks.size; j++){ if(blocks.get(j) == currentBlock){ switch(i){ case 10: //left j = (j - 1 + blocks.size) % blocks.size; break; case 11: //right j = (j + 1) % blocks.size; break; case 12: //up j = (j > 3 ? j - 4 : blocks.size - blocks.size % 4 + j); j -= (j < blocks.size ? 0 : 4); break; case 13: //down j = (j < blocks.size - 4 ? j + 4 : j % 4); } input.block = blocks.get(j); selectedBlocks.put(currentCategory, input.block); break; } } }else if(blockSelectEnd || Time.timeSinceMillis(blockSelectSeqMillis) > Core.settings.getInt("blockselecttimeout")){ //1st number of combo, select category //select only visible categories if(!getByCategory(Category.all[i]).isEmpty()){ currentCategory = Category.all[i]; if(input.block != null){ input.block = getSelectedBlock(currentCategory); } blockSelectEnd = false; blockSelectSeq = 0; blockSelectSeqMillis = Time.millis(); } }else{ //select block if(blockSelectSeq == 0){ //2nd number of combo blockSelectSeq = i + 1; }else{ //3rd number of combo //entering "X,1,0" selects the same block as "X,0" i += (blockSelectSeq - (i != 9 ? 0 : 1)) * 10; blockSelectEnd = true; } Array blocks = getByCategory(currentCategory); input.block = (i < blocks.size) ? blocks.get(i) : null; selectedBlocks.put(currentCategory, input.block); blockSelectSeqMillis = Time.millis(); } return true; } } if(Core.input.keyTap(Binding.category_prev)){ do{ currentCategory = currentCategory.prev(); }while(categoryEmpty[currentCategory.ordinal()]); input.block = getSelectedBlock(currentCategory); return true; } if(Core.input.keyTap(Binding.category_next)){ do{ currentCategory = currentCategory.next(); }while(categoryEmpty[currentCategory.ordinal()]); input.block = getSelectedBlock(currentCategory); return true; } return false; } @Override public void build(Group parent){ parent.fill(full -> { toggler = full; full.bottom().right().visible(() -> ui.hudfrag.shown()); full.table(frame -> { //rebuilds the category table with the correct recipes Runnable rebuildCategory = () -> { blockTable.clear(); blockTable.top().margin(5); int index = 0; ButtonGroup group = new ButtonGroup<>(); group.setMinCheckCount(0); for(Block block : getByCategory(currentCategory)){ if(index++ % rowWidth == 0){ blockTable.row(); } ImageButton button = blockTable.addImageButton(new TextureRegionDrawable(block.icon(Cicon.medium)), Styles.selecti, () -> { if(unlocked(block)){ if(Core.input.keyDown(KeyCode.SHIFT_LEFT) && Fonts.getUnicode(block.name) != 0){ Core.app.setClipboardText((char)Fonts.getUnicode(block.name) + ""); ui.showInfoFade("$copied"); }else{ control.input.block = control.input.block == block ? null : block; selectedBlocks.put(currentCategory, control.input.block); } } }).size(46f).group(group).name("block-" + block.name).get(); button.resizeImage(Cicon.medium.size); button.update(() -> { //color unplacable things gray Tilec core = player.closestCore(); Color color = (state.rules.infiniteResources || (core != null && (core.items().has(block.requirements, state.rules.buildCostMultiplier) || state.rules.infiniteResources))) && player.isBuilder() ? Color.white : Color.gray; button.forEach(elem -> elem.setColor(color)); button.setChecked(control.input.block == block); if(state.rules.bannedBlocks.contains(block)){ button.forEach(elem -> elem.setColor(Color.darkGray)); } }); button.hovered(() -> hovered = block); button.exited(() -> { if(hovered == block){ hovered = null; } }); } //add missing elements to even out table size if(index < 4){ for(int i = 0; i < 4-index; i++){ blockTable.add().size(46f); } } blockTable.act(0f); blockPane.setScrollYForce(scrollPositions.get(currentCategory, 0)); Core.app.post(() -> { blockPane.setScrollYForce(scrollPositions.get(currentCategory, 0)); blockPane.act(0f); blockPane.layout(); }); }; //top table with hover info frame.table(Tex.buttonEdge2,top -> { topTable = top; top.add(new Table()).growX().update(topTable -> { //don't refresh unnecessarily if((tileDisplayBlock() == null && lastDisplay == getSelected() && !lastGround) || (tileDisplayBlock() != null && lastHover == hoverTile && lastDisplay == tileDisplayBlock() && lastGround)) return; topTable.clear(); topTable.top().left().margin(5); lastHover = hoverTile; lastDisplay = getSelected(); lastGround = tileDisplayBlock() != null; if(lastDisplay != null){ //show selected recipe lastGround = false; topTable.table(header -> { String keyCombo = ""; if(!mobile && Core.settings.getBool("blockselectkeys")){ Array blocks = getByCategory(currentCategory); for(int i = 0; i < blocks.size; i++){ if(blocks.get(i) == lastDisplay && (i + 1) / 10 - 1 < blockSelect.length){ keyCombo = Core.bundle.format("placement.blockselectkeys", Core.keybinds.get(blockSelect[currentCategory.ordinal()]).key.toString()) + (i < 10 ? "" : Core.keybinds.get(blockSelect[(i + 1) / 10 - 1]).key.toString() + ",") + Core.keybinds.get(blockSelect[i % 10]).key.toString() + "]"; break; } } } final String keyComboFinal = keyCombo; header.left(); header.add(new Image(lastDisplay.icon(Cicon.medium))).size(8 * 4); header.labelWrap(() -> !unlocked(lastDisplay) ? Core.bundle.get("block.unknown") : lastDisplay.localizedName + keyComboFinal) .left().width(190f).padLeft(5); header.add().growX(); if(unlocked(lastDisplay)){ header.addButton("?", Styles.clearPartialt, () -> { ui.content.show(lastDisplay); Events.fire(new BlockInfoEvent()); }).size(8 * 5).padTop(-5).padRight(-5).right().grow().name("blockinfo"); } }).growX().left(); topTable.row(); //add requirement table topTable.table(req -> { req.top().left(); for(ItemStack stack : lastDisplay.requirements){ req.table(line -> { line.left(); line.addImage(stack.item.icon(Cicon.small)).size(8 * 2); line.add(stack.item.localizedName).maxWidth(140f).fillX().color(Color.lightGray).padLeft(2).left().get().setEllipsis(true); line.labelWrap(() -> { Tilec core = player.closestCore(); if(core == null || state.rules.infiniteResources) return "*/*"; int amount = core.items().get(stack.item); int stackamount = Math.round(stack.amount * state.rules.buildCostMultiplier); String color = (amount < stackamount / 2f ? "[red]" : amount < stackamount ? "[accent]" : "[white]"); return color + ui.formatAmount(amount) + "[white]/" + stackamount; }).padLeft(5); }).left(); req.row(); } }).growX().left().margin(3); if(state.rules.bannedBlocks.contains(lastDisplay) || !player.isBuilder()){ topTable.row(); topTable.table(b -> { b.addImage(Icon.cancel).padRight(2).color(Color.scarlet); b.add(!player.isBuilder() ? "$unit.nobuild" : "$banned"); b.left(); }).padTop(2).left(); } }else if(tileDisplayBlock() != null){ //show selected tile lastDisplay = tileDisplayBlock(); topTable.table(t -> { t.left(); t.add(new Image(lastDisplay.getDisplayIcon(hoverTile))).size(8 * 4); t.labelWrap(lastDisplay.getDisplayName(hoverTile)).left().width(190f).padLeft(5); }).growX().left(); if(hoverTile.team() == player.team()){ topTable.row(); topTable.table(t -> { t.left().defaults().left(); if(hoverTile.entity != null){ hoverTile.entity.display(t); } }).left().growX(); } } }); }).colspan(3).fillX().visible(() -> getSelected() != null || tileDisplayBlock() != null).touchable(Touchable.enabled); frame.row(); frame.addImage().color(Pal.gray).colspan(3).height(4).growX(); frame.row(); frame.table(Tex.pane2, blocksSelect -> { blocksSelect.margin(4).marginTop(0); blockPane = blocksSelect.pane(blocks -> blockTable = blocks).height(194f).update(pane -> { if(pane.hasScroll()){ Element result = Core.scene.hit(Core.input.mouseX(), Core.input.mouseY(), true); if(result == null || !result.isDescendantOf(pane)){ Core.scene.setScrollFocus(null); } } }).grow().get(); blockPane.setStyle(Styles.smallPane); blocksSelect.row(); blocksSelect.table(control.input::buildPlacementUI).name("inputTable").growX(); }).fillY().bottom().touchable(Touchable.enabled); frame.table(categories -> { categories.bottom(); categories.add(new Image(Styles.black6){ @Override public void draw(){ if(height <= Scl.scl(3f)) return; getDrawable().draw(x, y, width, height - Scl.scl(3f)); } }).colspan(2).growX().growY().padTop(-3f).row(); categories.defaults().size(50f); ButtonGroup group = new ButtonGroup<>(); //update category empty values for(Category cat : Category.all){ Array blocks = getByCategory(cat); categoryEmpty[cat.ordinal()] = blocks.isEmpty(); } int f = 0; for(Category cat : getCategories()){ if(f++ % 2 == 0) categories.row(); if(categoryEmpty[cat.ordinal()]){ categories.addImage(Styles.black6); continue; } categories.addImageButton(ui.getIcon(cat.name()), Styles.clearToggleTransi, () -> { currentCategory = cat; if(control.input.block != null){ control.input.block = getSelectedBlock(currentCategory); } rebuildCategory.run(); }).group(group).update(i -> i.setChecked(currentCategory == cat)).name("category-" + cat.name()); } }).fillY().bottom().touchable(Touchable.enabled); rebuildCategory.run(); frame.update(() -> { if(gridUpdate(control.input)) rebuildCategory.run(); }); }); }); } Array getCategories(){ returnCatArray.clear(); returnCatArray.addAll(Category.all); returnCatArray.sort((c1, c2) -> Boolean.compare(categoryEmpty[c1.ordinal()], categoryEmpty[c2.ordinal()])); return returnCatArray; } Array getByCategory(Category cat){ returnArray.clear(); for(Block block : content.blocks()){ if(block.category == cat && block.isVisible() && unlocked(block)){ returnArray.add(block); } } returnArray.sort((b1, b2) -> { int locked = -Boolean.compare(unlocked(b1), unlocked(b2)); if(locked != 0) return locked; return Boolean.compare(state.rules.bannedBlocks.contains(b1), state.rules.bannedBlocks.contains(b2)); }); return returnArray; } Block getSelectedBlock(Category cat){ if(selectedBlocks.get(cat) == null){ selectedBlocks.put(cat, getByCategory(cat).find(this::unlocked)); } return selectedBlocks.get(cat); } boolean unlocked(Block block){ return !state.isCampaign() || data.isUnlocked(block); } /** Returns the currently displayed block in the top box. */ Block getSelected(){ Block toDisplay = null; Vec2 v = topTable.stageToLocalCoordinates(Core.input.mouse()); //setup hovering tile if(!Core.scene.hasMouse() && topTable.hit(v.x, v.y, false) == null){ hoverTile = world.tileWorld(Core.input.mouseWorld().x, Core.input.mouseWorld().y); }else{ hoverTile = null; } //block currently selected if(control.input.block != null){ toDisplay = control.input.block; } //block hovered on in build menu if(hovered != null){ toDisplay = hovered; } return toDisplay; } /** Returns the block currently being hovered over in the world. */ Block tileDisplayBlock(){ return hoverTile == null ? null : hoverTile.block().synthetic() ? hoverTile.block() : hoverTile.drop() != null && hoverTile.block() == Blocks.air ? hoverTile.overlay().itemDrop != null ? hoverTile.overlay() : hoverTile.floor() : null; } }