diff --git a/src/UnitInfo/ui/EditorTool.java b/src/UnitInfo/ui/EditorTool.java new file mode 100644 index 0000000..a10bfe3 --- /dev/null +++ b/src/UnitInfo/ui/EditorTool.java @@ -0,0 +1,268 @@ +package UnitInfo.ui; + +import UnitInfo.ui.windows.WindowTables; +import arc.func.Boolf; +import arc.func.Cons; +import arc.input.KeyCode; +import arc.math.Mathf; +import arc.math.geom.Bresenham2; +import arc.math.geom.Point2; +import arc.struct.IntSeq; +import arc.util.Structs; +import mindustry.content.Blocks; +import mindustry.game.Team; +import mindustry.world.Block; +import mindustry.world.Tile; + +import static UnitInfo.ui.windows.MapEditorDisplay.drawTeam; +import static UnitInfo.ui.windows.MapEditorDisplay.selected; +import static UnitInfo.ui.windows.WindowTables.editorTable; +import static mindustry.Vars.world; + +public enum EditorTool{ + zoom(KeyCode.v), + pick(KeyCode.i){ + public void touched(int x, int y){ + if(!Structs.inBounds(x, y, world.width(), world.height())) return; + + Tile tile = world.tile(x, y); + selected = tile.block() == Blocks.air || !tile.block().inEditor ? tile.overlay() == Blocks.air ? tile.floor() : tile.overlay() : tile.block(); + } + }, + line(KeyCode.l, "replace", "orthogonal"){ + + @Override + public void touchedLine(int x1, int y1, int x2, int y2){ + //straight + if(mode == 1){ + if(Math.abs(x2 - x1) > Math.abs(y2 - y1)){ + y2 = y1; + }else{ + x2 = x1; + } + } + + Bresenham2.line(x1, y1, x2, y2, (x, y) -> { + if(mode == 0){ + //replace + editorTable.drawBlocksReplace(x, y); + }else{ + //normal + editorTable.drawBlocks(x, y); + } + }); + } + }, + pencil(KeyCode.b, "replace", "square", "drawteams"){ + { + edit = true; + draggable = true; + } + + @Override + public void touched(int x, int y){ + if(mode == -1){ + //normal mode + editorTable.drawBlocks(x, y); + }else if(mode == 0){ + //replace mode + editorTable.drawBlocksReplace(x, y); + }else if(mode == 1){ + //square mode + editorTable.drawBlocks(x, y, true, tile -> true); + }else if(mode == 2){ + //draw teams + editorTable.drawCircle(x, y, tile -> tile.setTeam(drawTeam)); + } + + } + }, + eraser(KeyCode.e, "eraseores"){ + { + edit = true; + draggable = true; + } + + @Override + public void touched(int x, int y){ + editorTable.drawCircle(x, y, tile -> { + if(mode == -1){ + //erase block + tile.remove(); + }else if(mode == 0){ + //erase ore + tile.clearOverlay(); + } + }); + } + }, + fill(KeyCode.g, "replaceall", "fillteams"){ + { + edit = true; + } + + IntSeq stack = new IntSeq(); + + @Override + public void touched(int x, int y){ + if(!Structs.inBounds(x, y, world.width(), world.height()) || world.tile(x, y).block()!=null&&world.tile(x, y).block().isMultiblock()) return; + Tile tile = world.tile(x, y); + + //mode 0 or 1, fill everything with the floor/tile or replace it + if(mode == 0 || mode == -1){ + if(tile.block().isMultiblock()) return; + + Boolf tester; + Cons setter; + Block drawBlock = selected; + + if(drawBlock.isOverlay()){ + Block dest = tile.overlay(); + if(dest == drawBlock) return; + tester = t -> t.overlay() == dest && (t.floor().hasSurface() || !t.floor().needsSurface); + setter = t -> t.setOverlay(drawBlock); + }else if(drawBlock.isFloor()){ + Block dest = tile.floor(); + if(dest == drawBlock) return; + tester = t -> t.floor() == dest; + setter = t -> t.setFloorUnder(drawBlock.asFloor()); + }else{ + Block dest = tile.block(); + if(dest == drawBlock) return; + tester = t -> t.block() == dest; + setter = t -> t.setBlock(drawBlock, drawTeam); + } + + //replace only when the mode is 0 using the specified functions + fill(x, y, mode == 0, tester, setter); + }else if(mode == 1){ //mode 1 is team fill + //only fill synthetic blocks, it's meaningless otherwise + if(tile.synthetic()){ + Team dest = tile.team(); + if(dest == drawTeam) return; + fill(x, y, false, t -> t.getTeamID() == dest.id && t.synthetic(), t -> t.setTeam(drawTeam)); + } + } + } + + void fill(int x, int y, boolean replace, Boolf tester, Cons filler){ + int width = world.width(), height = world.height(); + + if(replace){ + //just do it on everything + for(int cx = 0; cx < width; cx++){ + for(int cy = 0; cy < height; cy++){ + Tile tile = world.tile(cx, cy); + if(tester.get(tile)){ + filler.get(tile); + } + } + } + + }else{ + //perform flood fill + int x1; + + stack.clear(); + stack.add(Point2.pack(x, y)); + + try{ + while(stack.size > 0 && stack.size < width*height){ + int popped = stack.pop(); + x = Point2.x(popped); + y = Point2.y(popped); + + x1 = x; + while(x1 >= 0 && tester.get(world.tile(x1, y))) x1--; + x1++; + boolean spanAbove = false, spanBelow = false; + while(x1 < width && tester.get(world.tile(x1, y))){ + filler.get(world.tile(x1, y)); + + if(!spanAbove && y > 0 && tester.get(world.tile(x1, y - 1))){ + stack.add(Point2.pack(x1, y - 1)); + spanAbove = true; + }else if(spanAbove && !tester.get(world.tile(x1, y - 1))){ + spanAbove = false; + } + + if(!spanBelow && y < height - 1 && tester.get(world.tile(x1, y + 1))){ + stack.add(Point2.pack(x1, y + 1)); + spanBelow = true; + }else if(spanBelow && y < height - 1 && !tester.get(world.tile(x1, y + 1))){ + spanBelow = false; + } + x1++; + } + } + stack.clear(); + }catch(OutOfMemoryError e){ + //hack + stack = null; + System.gc(); + e.printStackTrace(); + stack = new IntSeq(); + } + } + } + }, + spray(KeyCode.r, "replace"){ + final double chance = 0.012; + + { + edit = true; + draggable = true; + } + + @Override + public void touched(int x, int y){ + //floor spray + if(selected.isFloor()){ + editorTable.drawCircle(x, y, tile -> { + if(Mathf.chance(chance)){ + tile.setFloor(selected.asFloor()); + } + }); + }else if(mode == 0){ //replace-only mode, doesn't affect air + editorTable.drawBlocks(x, y, tile -> Mathf.chance(chance) && tile.block() != Blocks.air); + }else{ + editorTable.drawBlocks(x, y, tile -> Mathf.chance(chance)); + } + } + }; + + public static final EditorTool[] all = values(); + + /** All the internal alternate placement modes of this tool. */ + public final String[] altModes; + /** Key to activate this tool. */ + public KeyCode key = KeyCode.unset; + /** The current alternate placement mode. -1 is the standard mode, no changes.*/ + public int mode = -1; + /** Whether this tool causes canvas changes when touched.*/ + public boolean edit; + /** Whether this tool should be dragged across the canvas when the mouse moves.*/ + public boolean draggable; + + EditorTool(){ + this(new String[]{}); + } + + EditorTool(KeyCode code){ + this(new String[]{}); + this.key = code; + } + + EditorTool(String... altModes){ + this.altModes = altModes; + } + + EditorTool(KeyCode code, String... altModes){ + this.altModes = altModes; + this.key = code; + } + + public void touched(int x, int y){} + + public void touchedLine(int x1, int y1, int x2, int y2){} +} diff --git a/src/UnitInfo/ui/HUDFragment.java b/src/UnitInfo/ui/HUDFragment.java index d669795..b1d34c7 100644 --- a/src/UnitInfo/ui/HUDFragment.java +++ b/src/UnitInfo/ui/HUDFragment.java @@ -19,7 +19,8 @@ public class HUDFragment extends Fragment{ waveTable, coreTable, playerTable, - toolTable + toolTable, + editorTable )).visible(TaskbarTable.visibility); // windows (totally not a copyright violation) @@ -28,6 +29,7 @@ public class HUDFragment extends Fragment{ t.add(coreTable).size(250f).visible(false); t.add(playerTable).size(250f).visible(false); t.add(toolTable).size(250f).visible(false); + t.add(editorTable).size(250f).visible(false); t.update(()->{ for (Element child : t.getChildren()) { diff --git a/src/UnitInfo/ui/draws/UtilDraw.java b/src/UnitInfo/ui/draws/UtilDraw.java index 94e7552..80c83c8 100644 --- a/src/UnitInfo/ui/draws/UtilDraw.java +++ b/src/UnitInfo/ui/draws/UtilDraw.java @@ -1,13 +1,26 @@ package UnitInfo.ui.draws; +import arc.Events; +import arc.func.Boolf; +import arc.func.Cons; import arc.input.KeyCode; import arc.math.Angles; import arc.math.geom.Geometry; +import arc.math.geom.Point2; import arc.scene.style.TextureRegionDrawable; +import arc.scene.ui.Button; +import arc.scene.ui.CheckBox; +import arc.scene.ui.layout.Table; +import arc.struct.IntSeq; +import arc.util.Structs; +import mindustry.content.Blocks; import mindustry.entities.Units; +import mindustry.game.EventType; import mindustry.game.Team; import mindustry.gen.*; import mindustry.logic.Ranged; +import mindustry.world.Block; +import mindustry.world.Tile; import mindustry.world.blocks.ControlBlock; import mindustry.world.blocks.defense.turrets.Turret; @@ -20,6 +33,7 @@ public class UtilDraw extends OverDraw { UtilDraw(String name, TextureRegionDrawable icon) { super(name, icon); registerOption("autoShooting"); + } @Override @@ -77,4 +91,5 @@ public class UtilDraw extends OverDraw { unit.controlWeapons(player.shooting && !boosted); } } + } diff --git a/src/UnitInfo/ui/windows/MapEditorDisplay.java b/src/UnitInfo/ui/windows/MapEditorDisplay.java new file mode 100644 index 0000000..ce11b51 --- /dev/null +++ b/src/UnitInfo/ui/windows/MapEditorDisplay.java @@ -0,0 +1,335 @@ +package UnitInfo.ui.windows; + +import UnitInfo.ui.EditorTool; +import UnitInfo.ui.OverScrollPane; +import UnitInfo.ui.Updatable; +import arc.Core; +import arc.Events; +import arc.func.Boolf; +import arc.func.Cons; +import arc.func.Prov; +import arc.graphics.Color; +import arc.input.KeyCode; +import arc.math.Mathf; +import arc.math.geom.Point2; +import arc.math.geom.Vec2; +import arc.scene.event.Touchable; +import arc.scene.style.TextureRegionDrawable; +import arc.scene.ui.*; +import arc.scene.ui.layout.Scl; +import arc.scene.ui.layout.Table; +import arc.scene.utils.Elem; +import arc.struct.IntSeq; +import arc.struct.Seq; +import arc.util.*; +import mindustry.Vars; +import mindustry.content.Blocks; +import mindustry.editor.DrawOperation; +import mindustry.game.EventType; +import mindustry.game.Team; +import mindustry.gen.Icon; +import mindustry.gen.Tex; +import mindustry.gen.TileOp; +import mindustry.graphics.Pal; +import mindustry.ui.Styles; +import mindustry.world.Block; +import mindustry.world.Tile; + +import static mindustry.Vars.*; + +public class MapEditorDisplay extends WindowTable implements Updatable { + Vec2 scrollPos = new Vec2(0, 0); + TextField search; + Table window; + float heat; + float brushSize = -1; + UnitInfo.ui.EditorTool tool; + + public static Team drawTeam = Team.sharded; + public static Block selected = Blocks.router; + + public MapEditorDisplay() { + super("Map Editor Display", Icon.map, t -> {}); + } + + @Override + public void build() { + scrollPos = new Vec2(0, 0); + search = Elem.newField(null, f->{}); + search.setMessageText("Search..."); + + top(); + topBar(); + + table(Styles.black8, table -> { + window=table; + table.table(t->{ + t.left().background(Tex.underline2); + t.label(()->"[accent]"+selected.localizedName+"[] "+selected.emoji()); + t.add(search).growX().pad(8).name("search"); + }).growX().row(); + table.add(new OverScrollPane(rebuild(), Styles.nonePane, scrollPos).disableScroll(true, false)).grow().name("editor-pane"); + }).top().right().grow().get().parent = null; + + resizeButton(); + } + + boolean hold = false; + int pastX, pastY; + @Override + public void update() { + heat += Time.delta; + if(heat >= 60f) { + heat = 0f; + resetPane(); + } + if(tool != null) { + if(Core.input.isTouched()) { + if(!(!mobile&&Core.input.keyDown(KeyCode.mouseLeft))) return; + if(tool== EditorTool.line) { + if(!hold) { + pastX = (int) player.mouseX / 8; + pastY = (int) player.mouseY / 8; + } + hold = true; + } + else { + pastX = (int) player.mouseX / 8; + pastY = (int) player.mouseY / 8; + } + + tool.touched(pastX, pastY); + } + else if(tool== EditorTool.line) { + if(hold&&pastX>=0&&pastY>=0) tool.touchedLine(pastX, pastY, (int) player.mouseX/8, (int) player.mouseY/8); + hold = false; + pastX = -1; + pastY = -1; + } + } + } + + void resetPane() { + ScrollPane pane = find("editor-pane"); + pane.setWidget(rebuild()); + } + + public Table rebuild() { + return new Table(table-> { + table.top(); + Seq blocks = Vars.content.blocks().copy(); + if(search.getText().length() > 0){ + blocks.filter(p -> p.name.toLowerCase().contains(search.getText().toLowerCase())||p.localizedName.toLowerCase().contains(search.getText().toLowerCase())); + } + table.table(select->buildTable(Blocks.boulder, select, blocks, ()->selected, block->selected=block, false)).marginTop(16f).marginBottom(16f).row(); + table.image().height(4f).color(Pal.gray).growX().row(); + table.table(body-> { + body.table(tools -> { + tools.top().left(); + tools.table(title -> title.left().background(Tex.underline2).add("Tools [accent]"+(tool==null?"":tool.name())+"[]")).growX().row(); + tools.table(bt->{ + Cons addTool = tool -> { + ImageButton button = new ImageButton(ui.getIcon(tool.name()), Styles.clearTogglei); + button.clicked(() -> { + button.toggle(); + if(this.tool==tool) this.tool = null; + else this.tool = tool; + resetPane(); + }); + button.update(()->button.setChecked(this.tool == tool)); + + Label mode = new Label(""); + mode.setColor(Pal.remove); + mode.update(() -> mode.setText(tool.mode == -1 ? "" : "M" + (tool.mode + 1) + " ")); + mode.setAlignment(Align.bottomRight, Align.bottomRight); + mode.touchable = Touchable.disabled; + + bt.stack(button, mode); + }; + + addTool.get(UnitInfo.ui.EditorTool.line); + addTool.get(UnitInfo.ui.EditorTool.pencil); + addTool.get(UnitInfo.ui.EditorTool.eraser); + addTool.get(UnitInfo.ui.EditorTool.fill); + addTool.get(UnitInfo.ui.EditorTool.spray); + }); + tools.row(); + Slider slider = new Slider(1, 16, 1, false); + slider.moved(size->brushSize=size); + slider.setValue(brushSize); + Label label = new Label("Brush: "+brushSize); + label.touchable = Touchable.disabled; + tools.stack(slider, label).width(window.getWidth()/5).center(); + }).left().width(window.getWidth() / 2).margin(8f).growY(); + body.image().width(4f).height(body.getHeight()).color(Pal.gray).growY(); + body.table(options -> { + options.top().left(); + options.table(title -> title.left().background(Tex.underline2).add("Options [accent]"+(tool!=null&&tool.mode>=0&&tool.mode { + option.defaults().size(300f, 70f).left(); + if(tool==null) return; + + for (int i = 0; i < tool.altModes.length; i++) { + int mode = i; + String name = tool.altModes[i]; + + option.button(b -> { + b.left().marginLeft(6); + b.setStyle(Styles.clearTogglet); + b.add(Core.bundle.get("toolmode." + name)).left().row(); + b.add(Core.bundle.get("toolmode." + name + ".description")).color(Color.lightGray).left(); + }, () -> { + tool.mode = (tool.mode == mode ? -1 : mode); + }).update(b -> b.setChecked(tool.mode == mode)); + option.row(); + } + }); + }).left().width(window.getWidth() / 2).margin(8f).growY(); + }).grow(); + }); + } + + void buildTable(@Nullable Block block, Table table, Seq items, Prov holder, Cons consumer, boolean closeSelect){ + ButtonGroup group = new ButtonGroup<>(); + group.setMinCheckCount(0); + Table cont = new Table(); + cont.defaults().size(40); + + int i = 0; + int max = Math.max(4, Math.round(window.getWidth()/64)); + + for(T item : items){ + if(!item.unlockedNow()) continue; + + ImageButton button = cont.button(Tex.whiteui, Styles.clearToggleTransi, 24, () -> { + if(closeSelect) control.input.frag.config.hideConfig(); + }).group(group).tooltip(t->t.background(Styles.black8).add(item.localizedName)).get(); + button.changed(() -> consumer.get(button.isChecked() ? item : null)); + button.getStyle().imageUp = new TextureRegionDrawable(item.uiIcon); + button.update(() -> button.setChecked(holder.get() == item)); + + if(i++ % max == max-1){ + cont.row(); + } + } + + //add extra blank spaces so it looks nice + if(i % max != 0){ + int remaining = max - (i % max); + for(int j = 0; j < remaining; j++){ + cont.image(Styles.black6); + } + } + + ScrollPane pane = new ScrollPane(cont, Styles.smallPane); + pane.setScrollingDisabled(true, false); + + if(block != null){ + pane.setScrollYForce(block.selectScroll); + pane.update(() -> { + block.selectScroll = pane.getScrollY(); + }); + } + + pane.setOverscroll(false, false); + table.add(pane).maxHeight(Scl.scl(40 * 5)); + } + + public void drawBlocksReplace(int x, int y){ + drawBlocks(x, y, tile -> tile.block() != Blocks.air || selected.isFloor()); + } + + public void drawBlocks(int x, int y){ + drawBlocks(x, y, false, tile -> true); + } + + public void drawBlocks(int x, int y, Boolf tester){ + drawBlocks(x, y, false, tester); + } + + int rotation = 0; + public void drawBlocks(int x, int y, boolean square, Boolf tester){ + if(selected.isMultiblock()){ + x = Mathf.clamp(x, (selected.size - 1) / 2, world.width() - selected.size / 2 - 1); + y = Mathf.clamp(y, (selected.size - 1) / 2, world.height() - selected.size / 2 - 1); + if(!hasOverlap(x, y)){ + world.tile(x, y).setBlock(selected, drawTeam, rotation); + } + }else{ + boolean isFloor = selected.isFloor() && selected != Blocks.air; + + Cons drawer = tile -> { + if(!tester.get(tile)) return; + + if(isFloor){ + tile.setFloor(selected.asFloor()); + }else if(!(tile.block().isMultiblock() && !selected.isMultiblock())){ + tile.setBlock(selected, drawTeam, rotation); + } + }; + + if(square){ + drawSquare(x, y, drawer); + }else{ + drawCircle(x, y, drawer); + } + } + } + + public void drawCircle(int x, int y, Cons drawer){ + int clamped = (int)brushSize; + for(int rx = -clamped; rx <= clamped; rx++){ + for(int ry = -clamped; ry <= clamped; ry++){ + if(Mathf.within(rx, ry, brushSize - 0.5f + 0.0001f)){ + int wx = x + rx, wy = y + ry; + + if(wx < 0 || wy < 0 || wx >= world.width() || wy >= world.height()){ + continue; + } + + drawer.get(world.tile(wx, wy)); + } + } + } + } + + public void drawSquare(int x, int y, Cons drawer){ + int clamped = (int)brushSize; + for(int rx = -clamped; rx <= clamped; rx++){ + for(int ry = -clamped; ry <= clamped; ry++){ + int wx = x + rx, wy = y + ry; + + if(wx < 0 || wy < 0 || wx >= world.width() || wy >= world.height()){ + continue; + } + + drawer.get(world.tile(wx, wy)); + } + } + } + + boolean hasOverlap(int x, int y){ + Tile tile = world.tile(x, y); + //allow direct replacement of blocks of the same size + if(tile != null && tile.isCenter() && tile.block() != selected && tile.block().size == selected.size && tile.x == x && tile.y == y){ + return false; + } + + //else, check for overlap + int offsetx = -(selected.size - 1) / 2; + int offsety = -(selected.size - 1) / 2; + for(int dx = 0; dx < selected.size; dx++){ + for(int dy = 0; dy < selected.size; dy++){ + int worldx = dx + offsetx + x; + int worldy = dy + offsety + y; + Tile other = world.tile(worldx, worldy); + + if(other != null && other.block().isMultiblock()){ + return true; + } + } + } + + return false; + } +} diff --git a/src/UnitInfo/ui/windows/PlayerDisplay.java b/src/UnitInfo/ui/windows/PlayerDisplay.java index bab1c34..a273038 100644 --- a/src/UnitInfo/ui/windows/PlayerDisplay.java +++ b/src/UnitInfo/ui/windows/PlayerDisplay.java @@ -23,9 +23,9 @@ import static mindustry.Vars.*; public class PlayerDisplay extends WindowTable implements Updatable { Vec2 scrollPos = new Vec2(0, 0); TextField search; + ImageButton.ImageButtonStyle ustyle; @Nullable Player target; float heat; - ImageButton.ImageButtonStyle ustyle; public PlayerDisplay() { super("Player Display", Icon.players, t -> {}); diff --git a/src/UnitInfo/ui/windows/WindowTables.java b/src/UnitInfo/ui/windows/WindowTables.java index 8d9df67..fddda4e 100644 --- a/src/UnitInfo/ui/windows/WindowTables.java +++ b/src/UnitInfo/ui/windows/WindowTables.java @@ -3,6 +3,7 @@ package UnitInfo.ui.windows; public class WindowTables { public static WindowTable unitTable, waveTable, coreTable, playerTable, toolTable; + public static MapEditorDisplay editorTable; public static void init() { unitTable = new UnitDisplay(); @@ -10,5 +11,6 @@ public class WindowTables { coreTable = new CoreDisplay(); playerTable = new PlayerDisplay(); toolTable = new ToolDisplay(); + editorTable = new MapEditorDisplay(); } }