it is done

This commit is contained in:
Anuken
2019-12-25 01:39:38 -05:00
parent 5b21873f3c
commit 514d4817c8
488 changed files with 4572 additions and 4574 deletions

View File

@@ -0,0 +1,95 @@
package mindustry.editor;
import mindustry.annotations.Annotations.Struct;
import arc.struct.LongArray;
import mindustry.game.Team;
import mindustry.gen.TileOp;
import mindustry.world.Block;
import mindustry.world.Tile;
import mindustry.world.blocks.Floor;
import static mindustry.Vars.content;
public class DrawOperation{
private MapEditor editor;
private LongArray array = new LongArray();
public DrawOperation(MapEditor editor) {
this.editor = editor;
}
public boolean isEmpty(){
return array.isEmpty();
}
public void addOperation(long op){
array.add(op);
}
public void undo(){
for(int i = array.size - 1; i >= 0; i--){
updateTile(i);
}
}
public void redo(){
for(int i = 0; i < array.size; i++){
updateTile(i);
}
}
private void updateTile(int i) {
long l = array.get(i);
array.set(i, TileOp.get(TileOp.x(l), TileOp.y(l), TileOp.type(l), getTile(editor.tile(TileOp.x(l), TileOp.y(l)), TileOp.type(l))));
setTile(editor.tile(TileOp.x(l), TileOp.y(l)), TileOp.type(l), TileOp.value(l));
}
short getTile(Tile tile, byte type){
if(type == OpType.floor.ordinal()){
return tile.floorID();
}else if(type == OpType.block.ordinal()){
return tile.blockID();
}else if(type == OpType.rotation.ordinal()){
return tile.rotation();
}else if(type == OpType.team.ordinal()){
return tile.getTeamID();
}else if(type == OpType.overlay.ordinal()){
return tile.overlayID();
}
throw new IllegalArgumentException("Invalid type.");
}
void setTile(Tile tile, byte type, short to){
editor.load(() -> {
if(type == OpType.floor.ordinal()){
tile.setFloor((Floor)content.block(to));
}else if(type == OpType.block.ordinal()){
Block block = content.block(to);
tile.setBlock(block, tile.getTeam(), tile.rotation());
}else if(type == OpType.rotation.ordinal()){
tile.rotation(to);
}else if(type == OpType.team.ordinal()){
tile.setTeam(Team.all[to]);
}else if(type == OpType.overlay.ordinal()){
tile.setOverlayID(to);
}
});
editor.renderer().updatePoint(tile.x, tile.y);
}
@Struct
class TileOpStruct{
short x;
short y;
byte type;
short value;
}
public enum OpType{
floor,
block,
rotation,
team,
overlay
}
}

View File

@@ -0,0 +1,153 @@
package mindustry.editor;
import mindustry.content.Blocks;
import mindustry.core.GameState.State;
import mindustry.editor.DrawOperation.OpType;
import mindustry.game.Team;
import mindustry.gen.TileOp;
import mindustry.world.Block;
import mindustry.world.Tile;
import mindustry.world.blocks.*;
import mindustry.world.modules.*;
import static mindustry.Vars.state;
import static mindustry.Vars.ui;
//TODO somehow remove or replace this class with a more flexible solution
public class EditorTile extends Tile{
public EditorTile(int x, int y, int floor, int overlay, int wall){
super(x, y, floor, overlay, wall);
}
@Override
public void setFloor(Floor type){
if(state.is(State.playing)){
super.setFloor(type);
return;
}
if(type instanceof OverlayFloor){
//don't place on liquids
if(!floor.isLiquid){
setOverlayID(type.id);
}
return;
}
if(floor == type && overlayID() == 0) return;
if(overlayID() != 0) op(OpType.overlay, overlayID());
if(floor != type) op(OpType.floor, floor.id);
super.setFloor(type);
}
@Override
public void setBlock(Block type){
if(state.is(State.playing)){
super.setBlock(type);
return;
}
if(block == type) return;
op(OpType.block, block.id);
if(rotation != 0) op(OpType.rotation, rotation);
if(team != 0) op(OpType.team, team);
super.setBlock(type);
}
@Override
public void setBlock(Block type, Team team, int rotation){
if(state.is(State.playing)){
super.setBlock(type, team, rotation);
return;
}
setBlock(type);
setTeam(team);
rotation(rotation);
}
@Override
public void setTeam(Team team){
if(state.is(State.playing)){
super.setTeam(team);
return;
}
if(getTeamID() == team.ordinal()) return;
op(OpType.team, getTeamID());
super.setTeam(team);
}
@Override
public void rotation(int rotation){
if(state.is(State.playing)){
super.rotation(rotation);
return;
}
if(rotation == rotation()) return;
op(OpType.rotation, rotation());
super.rotation(rotation);
}
@Override
public void setOverlay(Block overlay){
setOverlayID(overlay.id);
}
@Override
public void setOverlayID(short overlay){
if(state.is(State.playing)){
super.setOverlayID(overlay);
return;
}
if(floor.isLiquid) return;
if(overlayID() == overlay) return;
op(OpType.overlay, this.overlay.id);
super.setOverlayID(overlay);
}
@Override
protected void preChanged(){
if(state.is(State.playing)){
super.preChanged();
return;
}
super.setTeam(Team.derelict);
}
@Override
protected void changed(){
if(state.is(State.playing)){
super.changed();
return;
}
entity = null;
if(block == null){
block = Blocks.air;
}
if(floor == null){
floor = (Floor)Blocks.air;
}
Block block = block();
if(block.hasEntity()){
entity = block.newEntity().init(this, false);
entity.cons = new ConsumeModule(entity);
if(block.hasItems) entity.items = new ItemModule();
if(block.hasLiquids) entity.liquids = new LiquidModule();
if(block.hasPower) entity.power = new PowerModule();
}
}
private void op(OpType type, short value){
ui.editor.editor.addTileOp(TileOp.get(x, y, (byte)type.ordinal(), value));
}
}

View File

@@ -0,0 +1,247 @@
package mindustry.editor;
import arc.struct.IntArray;
import arc.func.*;
import arc.math.Mathf;
import arc.math.geom.Bresenham2;
import arc.util.Structs;
import mindustry.Vars;
import mindustry.content.Blocks;
import mindustry.game.Team;
import mindustry.world.*;
import mindustry.world.blocks.BlockPart;
public enum EditorTool{
zoom,
pick{
public void touched(MapEditor editor, int x, int y){
if(!Structs.inBounds(x, y, editor.width(), editor.height())) return;
Tile tile = editor.tile(x, y).link();
editor.drawBlock = tile.block() == Blocks.air ? tile.overlay() == Blocks.air ? tile.floor() : tile.overlay() : tile.block();
}
},
line("replace", "orthogonal"){
@Override
public void touchedLine(MapEditor editor, 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
editor.drawBlocksReplace(x, y);
}else{
//normal
editor.drawBlocks(x, y);
}
});
}
},
pencil("replace", "square", "drawteams"){
{
edit = true;
draggable = true;
}
@Override
public void touched(MapEditor editor, int x, int y){
if(mode == -1){
//normal mode
editor.drawBlocks(x, y);
}else if(mode == 0){
//replace mode
editor.drawBlocksReplace(x, y);
}else if(mode == 1){
//square mode
editor.drawBlocks(x, y, true, tile -> true);
}else if(mode == 2){
//draw teams
editor.drawCircle(x, y, tile -> tile.link().setTeam(editor.drawTeam));
}
}
},
eraser("eraseores"){
{
edit = true;
draggable = true;
}
@Override
public void touched(MapEditor editor, int x, int y){
editor.drawCircle(x, y, tile -> {
if(mode == -1){
//erase block
Vars.world.removeBlock(tile);
}else if(mode == 0){
//erase ore
tile.clearOverlay();
}
});
}
},
fill("replaceall", "fillteams"){
{
edit = true;
}
IntArray stack = new IntArray();
@Override
public void touched(MapEditor editor, int x, int y){
if(!Structs.inBounds(x, y, editor.width(), editor.height())) return;
Tile tile = editor.tile(x, y);
if(editor.drawBlock.isMultiblock()){
//don't fill multiblocks, thanks
pencil.touched(editor, x, y);
return;
}
//mode 0 or 1, fill everything with the floor/tile or replace it
if(mode == 0 || mode == -1){
//can't fill parts or multiblocks
if(tile.block() instanceof BlockPart || tile.block().isMultiblock()){
return;
}
Boolf<Tile> tester;
Cons<Tile> setter;
if(editor.drawBlock.isOverlay()){
Block dest = tile.overlay();
if(dest == editor.drawBlock) return;
tester = t -> t.overlay() == dest;
setter = t -> t.setOverlay(editor.drawBlock);
}else if(editor.drawBlock.isFloor()){
Block dest = tile.floor();
if(dest == editor.drawBlock) return;
tester = t -> t.floor() == dest;
setter = t -> t.setFloorUnder(editor.drawBlock.asFloor());
}else{
Block dest = tile.block();
if(dest == editor.drawBlock) return;
tester = t -> t.block() == dest;
setter = t -> t.setBlock(editor.drawBlock, editor.drawTeam);
}
//replace only when the mode is 0 using the specified functions
fill(editor, 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.link().synthetic()){
Team dest = tile.getTeam();
if(dest == editor.drawTeam) return;
fill(editor, x, y, false, t -> t.getTeamID() == dest.ordinal() && t.link().synthetic(), t -> t.setTeam(editor.drawTeam));
}
}
}
void fill(MapEditor editor, int x, int y, boolean replace, Boolf<Tile> tester, Cons<Tile> filler){
int width = editor.width(), height = editor.height();
if(replace){
//just do it on everything
for(int cx = 0; cx < width; cx++){
for(int cy = 0; cy < height; cy++){
Tile tile = editor.tile(cx, cy);
if(tester.get(tile)){
filler.get(tile);
}
}
}
}else{
//perform flood fill
int x1;
stack.clear();
stack.add(Pos.get(x, y));
while(stack.size > 0){
int popped = stack.pop();
x = Pos.x(popped);
y = Pos.y(popped);
x1 = x;
while(x1 >= 0 && tester.get(editor.tile(x1, y))) x1--;
x1++;
boolean spanAbove = false, spanBelow = false;
while(x1 < width && tester.get(editor.tile(x1, y))){
filler.get(editor.tile(x1, y));
if(!spanAbove && y > 0 && tester.get(editor.tile(x1, y - 1))){
stack.add(Pos.get(x1, y - 1));
spanAbove = true;
}else if(spanAbove && !tester.get(editor.tile(x1, y - 1))){
spanAbove = false;
}
if(!spanBelow && y < height - 1 && tester.get(editor.tile(x1, y + 1))){
stack.add(Pos.get(x1, y + 1));
spanBelow = true;
}else if(spanBelow && y < height - 1 && !tester.get(editor.tile(x1, y + 1))){
spanBelow = false;
}
x1++;
}
}
}
}
},
spray("replace"){
final double chance = 0.012;
{
edit = true;
draggable = true;
}
@Override
public void touched(MapEditor editor, int x, int y){
//floor spray
if(editor.drawBlock.isFloor()){
editor.drawCircle(x, y, tile -> {
if(Mathf.chance(chance)){
tile.setFloor(editor.drawBlock.asFloor());
}
});
}else if(mode == 0){ //replace-only mode, doesn't affect air
editor.drawBlocks(x, y, tile -> Mathf.chance(chance) && tile.block() != Blocks.air);
}else{
editor.drawBlocks(x, y, tile -> Mathf.chance(chance));
}
}
};
/** All the internal alternate placement modes of this tool. */
public final String[] altModes;
/** 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(String... altModes){
this.altModes = altModes;
}
public void touched(MapEditor editor, int x, int y){}
public void touchedLine(MapEditor editor, int x1, int y1, int x2, int y2){}
}

View File

@@ -0,0 +1,341 @@
package mindustry.editor;
import arc.struct.StringMap;
import arc.files.Fi;
import arc.func.Cons;
import arc.func.Boolf;
import arc.graphics.Pixmap;
import arc.math.Mathf;
import arc.util.Structs;
import mindustry.content.Blocks;
import mindustry.game.Team;
import mindustry.gen.TileOp;
import mindustry.io.LegacyMapIO;
import mindustry.io.MapIO;
import mindustry.maps.Map;
import mindustry.world.*;
import mindustry.world.blocks.BlockPart;
import static mindustry.Vars.*;
public class MapEditor{
public static final int[] brushSizes = {1, 2, 3, 4, 5, 9, 15, 20};
private final Context context = new Context();
private StringMap tags = new StringMap();
private MapRenderer renderer = new MapRenderer(this);
private OperationStack stack = new OperationStack();
private DrawOperation currentOp;
private boolean loading;
public int brushSize = 1;
public int rotation;
public Block drawBlock = Blocks.stone;
public Team drawTeam = Team.sharded;
public StringMap getTags(){
return tags;
}
public void beginEdit(int width, int height){
reset();
loading = true;
createTiles(width, height);
renderer.resize(width(), height());
loading = false;
}
public void beginEdit(Map map){
reset();
loading = true;
tags.putAll(map.tags);
if(map.file.parent().parent().name().equals("1127400") && steam){
tags.put("steamid", map.file.parent().name());
}
MapIO.loadMap(map, context);
checkLinkedTiles();
renderer.resize(width(), height());
loading = false;
}
public void beginEdit(Pixmap pixmap){
reset();
createTiles(pixmap.getWidth(), pixmap.getHeight());
load(() -> LegacyMapIO.readPixmap(pixmap, tiles()));
renderer.resize(width(), height());
}
//adds missing blockparts
public void checkLinkedTiles(){
Tile[][] tiles = world.getTiles();
//clear block parts first
for(int x = 0; x < width(); x++){
for(int y = 0; y < height(); y++){
if(tiles[x][y].block() instanceof BlockPart){
tiles[x][y].setBlock(Blocks.air);
}
}
}
//set up missing blockparts
for(int x = 0; x < width(); x++){
for(int y = 0; y < height(); y++){
if(tiles[x][y].block().isMultiblock()){
world.setBlock(tiles[x][y], tiles[x][y].block(), tiles[x][y].getTeam());
}
}
}
}
public void load(Runnable r){
loading = true;
r.run();
loading = false;
}
/** Creates a 2-D array of EditorTiles with stone as the floor block. */
private void createTiles(int width, int height){
Tile[][] tiles = world.createTiles(width, height);
for(int x = 0; x < width; x++){
for(int y = 0; y < height; y++){
tiles[x][y] = new EditorTile(x, y, Blocks.stone.id, (short)0, (short)0);
}
}
}
public Map createMap(Fi file){
return new Map(file, width(), height(), new StringMap(tags), true);
}
private void reset(){
clearOp();
brushSize = 1;
drawBlock = Blocks.stone;
tags = new StringMap();
}
public Tile[][] tiles(){
return world.getTiles();
}
public Tile tile(int x, int y){
return world.rawTile(x, y);
}
public int width(){
return world.width();
}
public int height(){
return world.height();
}
public void drawBlocksReplace(int x, int y){
drawBlocks(x, y, tile -> tile.block() != Blocks.air || drawBlock.isFloor());
}
public void drawBlocks(int x, int y){
drawBlocks(x, y, false, tile -> true);
}
public void drawBlocks(int x, int y, Boolf<Tile> tester){
drawBlocks(x, y, false, tester);
}
public void drawBlocks(int x, int y, boolean square, Boolf<Tile> tester){
if(drawBlock.isMultiblock()){
x = Mathf.clamp(x, (drawBlock.size - 1) / 2, width() - drawBlock.size / 2 - 1);
y = Mathf.clamp(y, (drawBlock.size - 1) / 2, height() - drawBlock.size / 2 - 1);
int offsetx = -(drawBlock.size - 1) / 2;
int offsety = -(drawBlock.size - 1) / 2;
for(int dx = 0; dx < drawBlock.size; dx++){
for(int dy = 0; dy < drawBlock.size; dy++){
int worldx = dx + offsetx + x;
int worldy = dy + offsety + y;
if(Structs.inBounds(worldx, worldy, width(), height())){
Tile tile = tile(worldx, worldy);
Block block = tile.block();
//bail out if there's anything blocking the way
if(block.isMultiblock() || block instanceof BlockPart){
return;
}
renderer.updatePoint(worldx, worldy);
}
}
}
world.setBlock(tile(x, y), drawBlock, drawTeam);
}else{
boolean isFloor = drawBlock.isFloor() && drawBlock != Blocks.air;
Cons<Tile> drawer = tile -> {
if(!tester.get(tile)) return;
//remove linked tiles blocking the way
if(!isFloor && (tile.isLinked() || tile.block().isMultiblock())){
world.removeBlock(tile.link());
}
if(isFloor){
tile.setFloor(drawBlock.asFloor());
}else{
tile.setBlock(drawBlock);
if(drawBlock.synthetic()){
tile.setTeam(drawTeam);
}
if(drawBlock.rotate){
tile.rotation((byte)rotation);
}
}
};
if(square){
drawSquare(x, y, drawer);
}else{
drawCircle(x, y, drawer);
}
}
}
public void drawCircle(int x, int y, Cons<Tile> drawer){
for(int rx = -brushSize; rx <= brushSize; rx++){
for(int ry = -brushSize; ry <= brushSize; ry++){
if(Mathf.dst2(rx, ry) <= (brushSize - 0.5f) * (brushSize - 0.5f)){
int wx = x + rx, wy = y + ry;
if(wx < 0 || wy < 0 || wx >= width() || wy >= height()){
continue;
}
drawer.get(tile(wx, wy));
}
}
}
}
public void drawSquare(int x, int y, Cons<Tile> drawer){
for(int rx = -brushSize; rx <= brushSize; rx++){
for(int ry = -brushSize; ry <= brushSize; ry++){
int wx = x + rx, wy = y + ry;
if(wx < 0 || wy < 0 || wx >= width() || wy >= height()){
continue;
}
drawer.get(tile(wx, wy));
}
}
}
public MapRenderer renderer(){
return renderer;
}
public void resize(int width, int height){
clearOp();
Tile[][] previous = world.getTiles();
int offsetX = -(width - width()) / 2, offsetY = -(height - height()) / 2;
loading = true;
Tile[][] tiles = world.createTiles(width, height);
for(int x = 0; x < width; x++){
for(int y = 0; y < height; y++){
int px = offsetX + x, py = offsetY + y;
if(Structs.inBounds(px, py, previous.length, previous[0].length)){
tiles[x][y] = previous[px][py];
tiles[x][y].x = (short)x;
tiles[x][y].y = (short)y;
}else{
tiles[x][y] = new EditorTile(x, y, Blocks.stone.id, (short)0, (short)0);
}
}
}
renderer.resize(width, height);
loading = false;
}
public void clearOp(){
stack.clear();
}
public void undo(){
if(stack.canUndo()){
stack.undo();
}
}
public void redo(){
if(stack.canRedo()){
stack.redo();
}
}
public boolean canUndo(){
return stack.canUndo();
}
public boolean canRedo(){
return stack.canRedo();
}
public void flushOp(){
if(currentOp == null || currentOp.isEmpty()) return;
stack.add(currentOp);
currentOp = null;
}
public void addTileOp(long data){
if(loading) return;
if(currentOp == null) currentOp = new DrawOperation(this);
currentOp.addOperation(data);
renderer.updatePoint(TileOp.x(data), TileOp.y(data));
}
class Context implements WorldContext{
@Override
public Tile tile(int x, int y){
return world.tile(x, y);
}
@Override
public void resize(int width, int height){
world.createTiles(width, height);
}
@Override
public Tile create(int x, int y, int floorID, int overlayID, int wallID){
return (tiles()[x][y] = new EditorTile(x, y, floorID, overlayID, wallID));
}
@Override
public boolean isGenerating(){
return world.isGenerating();
}
@Override
public void begin(){
world.beginMapLoad();
}
@Override
public void end(){
world.endMapLoad();
}
}
}

View File

@@ -0,0 +1,730 @@
package mindustry.editor;
import arc.*;
import arc.struct.*;
import arc.files.*;
import arc.func.*;
import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.input.*;
import arc.math.*;
import arc.math.geom.*;
import arc.scene.actions.*;
import arc.scene.event.*;
import arc.scene.style.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.util.*;
import arc.util.ArcAnnotate.*;
import mindustry.*;
import mindustry.content.*;
import mindustry.core.GameState.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.io.*;
import mindustry.maps.*;
import mindustry.ui.*;
import mindustry.ui.Cicon;
import mindustry.ui.dialogs.*;
import mindustry.world.*;
import mindustry.world.blocks.*;
import mindustry.world.blocks.storage.*;
import static mindustry.Vars.*;
public class MapEditorDialog extends Dialog implements Disposable{
public final MapEditor editor;
private MapView view;
private MapInfoDialog infoDialog;
private MapLoadDialog loadDialog;
private MapResizeDialog resizeDialog;
private MapGenerateDialog generateDialog;
private ScrollPane pane;
private FloatingDialog menu;
private Rules lastSavedRules;
private boolean saved = false;
private boolean shownWithMap = false;
private Array<Block> blocksOut = new Array<>();
public MapEditorDialog(){
super("");
background(Styles.black);
editor = new MapEditor();
view = new MapView(editor);
infoDialog = new MapInfoDialog(editor);
generateDialog = new MapGenerateDialog(editor, true);
menu = new FloatingDialog("$menu");
menu.addCloseButton();
float swidth = 180f;
menu.cont.table(t -> {
t.defaults().size(swidth, 60f).padBottom(5).padRight(5).padLeft(5);
t.addImageTextButton("$editor.savemap", Icon.floppy16Small, this::save);
t.addImageTextButton("$editor.mapinfo", Icon.pencilSmall, () -> {
infoDialog.show();
menu.hide();
});
t.row();
t.addImageTextButton("$editor.generate", Icon.editorSmall, () -> {
generateDialog.show(generateDialog::applyToEditor);
menu.hide();
});
t.addImageTextButton("$editor.resize", Icon.resizeSmall, () -> {
resizeDialog.show();
menu.hide();
});
t.row();
t.addImageTextButton("$editor.import", Icon.loadMapSmall, () ->
createDialog("$editor.import",
"$editor.importmap", "$editor.importmap.description", Icon.loadMap, (Runnable)loadDialog::show,
"$editor.importfile", "$editor.importfile.description", Icon.file, (Runnable)() ->
platform.showFileChooser(true, mapExtension, file -> ui.loadAnd(() -> {
maps.tryCatchMapError(() -> {
if(MapIO.isImage(file)){
ui.showInfo("$editor.errorimage");
}else{
editor.beginEdit(MapIO.createMap(file, true));
}
});
})),
"$editor.importimage", "$editor.importimage.description", Icon.fileImage, (Runnable)() ->
platform.showFileChooser(true, "png", file ->
ui.loadAnd(() -> {
try{
Pixmap pixmap = new Pixmap(file);
editor.beginEdit(pixmap);
pixmap.dispose();
}catch(Exception e){
ui.showException("$editor.errorload", e);
Log.err(e);
}
})))
);
t.addImageTextButton("$editor.export", Icon.saveMapSmall, () -> {
if(!ios){
platform.showFileChooser(false, mapExtension, file -> {
ui.loadAnd(() -> {
try{
if(!editor.getTags().containsKey("name")){
editor.getTags().put("name", file.nameWithoutExtension());
}
MapIO.writeMap(file, editor.createMap(file));
}catch(Exception e){
ui.showException("$editor.errorsave", e);
Log.err(e);
}
});
});
}else{
ui.loadAnd(() -> {
try{
Fi result = Core.files.local(editor.getTags().get("name", "unknown") + "." + mapExtension);
MapIO.writeMap(result, editor.createMap(result));
platform.shareFile(result);
}catch(Exception e){
ui.showException("$editor.errorsave", e);
Log.err(e);
}
});
}
});
});
menu.cont.row();
if(steam){
menu.cont.addImageTextButton("$editor.publish.workshop", Icon.linkSmall, () -> {
Map builtin = maps.all().find(m -> m.name().equals(editor.getTags().get("name", "").trim()));
if(editor.getTags().containsKey("steamid") && builtin != null && !builtin.custom){
platform.viewListingID(editor.getTags().get("steamid"));
return;
}
Map map = save();
if(editor.getTags().containsKey("steamid") && map != null){
platform.viewListing(map);
return;
}
if(map == null) return;
if(map.tags.get("description", "").length() < 4){
ui.showErrorMessage("$editor.nodescription");
return;
}
if(!Structs.contains(Gamemode.all, g -> g.valid(map))){
ui.showErrorMessage("$map.nospawn");
return;
}
platform.publish(map);
}).padTop(-3).size(swidth * 2f + 10, 60f).update(b -> b.setText(editor.getTags().containsKey("steamid") ? editor.getTags().get("author").equals(player.name) ? "$workshop.listing" : "$view.workshop" : "$editor.publish.workshop"));
menu.cont.row();
}
menu.cont.addImageTextButton("$editor.ingame", Icon.arrowSmall, this::playtest).padTop(!steam ? -3 : 1).size(swidth * 2f + 10, 60f);
menu.cont.row();
menu.cont.addImageTextButton("$quit", Icon.backSmall, () -> {
tryExit();
menu.hide();
}).size(swidth * 2f + 10, 60f);
resizeDialog = new MapResizeDialog(editor, (x, y) -> {
if(!(editor.width() == x && editor.height() == y)){
ui.loadAnd(() -> {
editor.resize(x, y);
});
}
});
loadDialog = new MapLoadDialog(map -> ui.loadAnd(() -> {
try{
editor.beginEdit(map);
}catch(Exception e){
ui.showException("$editor.errorload", e);
Log.err(e);
}
}));
setFillParent(true);
clearChildren();
margin(0);
update(() -> {
if(Core.scene.getKeyboardFocus() instanceof Dialog && Core.scene.getKeyboardFocus() != this){
return;
}
if(Core.scene != null && Core.scene.getKeyboardFocus() == this){
doInput();
}
});
shown(() -> {
saved = true;
if(!Core.settings.getBool("landscape")) platform.beginForceLandscape();
editor.clearOp();
Core.scene.setScrollFocus(view);
if(!shownWithMap){
//clear units, rules and other unnecessary stuff
logic.reset();
state.rules = new Rules();
editor.beginEdit(200, 200);
}
shownWithMap = false;
Time.runTask(10f, platform::updateRPC);
});
hidden(() -> {
editor.clearOp();
platform.updateRPC();
if(!Core.settings.getBool("landscape")) platform.endForceLandscape();
});
shown(this::build);
}
public void resumeEditing(){
state.set(State.menu);
shownWithMap = true;
show();
state.rules = (lastSavedRules == null ? new Rules() : lastSavedRules);
lastSavedRules = null;
editor.renderer().updateAll();
}
private void playtest(){
menu.hide();
ui.loadAnd(() -> {
lastSavedRules = state.rules;
hide();
//only reset the player; logic.reset() will clear entities, which we do not want
state.teams = new Teams();
player.reset();
state.rules = Gamemode.editor.apply(lastSavedRules.copy());
state.rules.zone = null;
world.setMap(new Map(StringMap.of(
"name", "Editor Playtesting",
"width", editor.width(),
"height", editor.height()
)));
world.endMapLoad();
//add entities so they update. is this really needed?
for(int x = 0; x < world.width(); x++){
for(int y = 0; y < world.height(); y++){
Tile tile = world.rawTile(x, y);
if(tile.entity != null){
tile.entity.add();
}
}
}
player.set(world.width() * tilesize/2f, world.height() * tilesize/2f);
player.setDead(false);
logic.play();
});
}
public @Nullable Map save(){
boolean isEditor = state.rules.editor;
state.rules.editor = false;
String name = editor.getTags().get("name", "").trim();
editor.getTags().put("rules", JsonIO.write(state.rules));
editor.getTags().remove("width");
editor.getTags().remove("height");
player.dead = true;
Map returned = null;
if(name.isEmpty()){
infoDialog.show();
Core.app.post(() -> ui.showErrorMessage("$editor.save.noname"));
}else{
Map map = maps.all().find(m -> m.name().equals(name));
if(map != null && !map.custom){
handleSaveBuiltin(map);
}else{
returned = maps.saveMap(editor.getTags());
ui.showInfoFade("$editor.saved");
}
}
menu.hide();
saved = true;
state.rules.editor = isEditor;
return returned;
}
/** Called when a built-in map save is attempted.*/
protected void handleSaveBuiltin(Map map){
ui.showErrorMessage("$editor.save.overwrite");
}
/**
* Argument format:
* 0) button name
* 1) description
* 2) icon name
* 3) listener
*/
private void createDialog(String title, Object... arguments){
FloatingDialog dialog = new FloatingDialog(title);
float h = 90f;
dialog.cont.defaults().size(360f, h).padBottom(5).padRight(5).padLeft(5);
for(int i = 0; i < arguments.length; i += 4){
String name = (String)arguments[i];
String description = (String)arguments[i + 1];
Drawable iconname = (Drawable)arguments[i + 2];
Runnable listenable = (Runnable)arguments[i + 3];
TextButton button = dialog.cont.addButton(name, () -> {
listenable.run();
dialog.hide();
menu.hide();
}).left().margin(0).get();
button.clearChildren();
button.addImage(iconname).padLeft(10);
button.table(t -> {
t.add(name).growX().wrap();
t.row();
t.add(description).color(Color.gray).growX().wrap();
}).growX().pad(10f).padLeft(5);
button.row();
dialog.cont.row();
}
dialog.addCloseButton();
dialog.show();
}
@Override
public Dialog show(){
return super.show(Core.scene, Actions.sequence(Actions.alpha(1f)));
}
@Override
public void hide(){
super.hide(Actions.sequence(Actions.alpha(0f)));
}
@Override
public void dispose(){
editor.renderer().dispose();
}
public void beginEditMap(Fi file){
ui.loadAnd(() -> {
try{
shownWithMap = true;
editor.beginEdit(MapIO.createMap(file, true));
show();
}catch(Exception e){
Log.err(e);
ui.showException("$editor.errorload", e);
}
});
}
public MapView getView(){
return view;
}
public MapGenerateDialog getGenerateDialog(){
return generateDialog;
}
public void resetSaved(){
saved = false;
}
public boolean hasPane(){
return Core.scene.getScrollFocus() == pane || Core.scene.getKeyboardFocus() != this;
}
public void build(){
float size = 60f;
clearChildren();
table(cont -> {
cont.left();
cont.table(mid -> {
mid.top();
Table tools = new Table().top();
ButtonGroup<ImageButton> group = new ButtonGroup<>();
Table[] lastTable = {null};
Cons<EditorTool> addTool = tool -> {
ImageButton button = new ImageButton(Core.atlas.drawable("icon-" + tool.name() + "-small"), Styles.clearTogglei);
button.clicked(() -> {
view.setTool(tool);
if(lastTable[0] != null){
lastTable[0].remove();
}
});
button.update(() -> button.setChecked(view.getTool() == tool));
group.add(button);
if(tool.altModes.length > 0){
button.clicked(l -> {
if(!mobile){
//desktop: rightclick
l.setButton(KeyCode.MOUSE_RIGHT);
}
}, e -> {
//need to double tap
if(mobile && e.getTapCount() < 2){
return;
}
if(lastTable[0] != null){
lastTable[0].remove();
}
Table table = new Table(Styles.black9);
table.defaults().size(300f, 70f);
for(int i = 0; i < tool.altModes.length; i++){
int mode = i;
String name = tool.altModes[i];
table.addButton(b -> {
b.left();
b.marginLeft(6);
b.setStyle(Styles.clearTogglet);
b.add(Core.bundle.get("toolmode." + name)).left();
b.row();
b.add(Core.bundle.get("toolmode." + name + ".description")).color(Color.lightGray).left();
}, () -> {
tool.mode = (tool.mode == mode ? -1 : mode);
table.remove();
}).update(b -> b.setChecked(tool.mode == mode));
table.row();
}
table.update(() -> {
Vector2 v = button.localToStageCoordinates(Tmp.v1.setZero());
table.setPosition(v.x, v.y, Align.topLeft);
if(!isShown()){
table.remove();
lastTable[0] = null;
}
});
table.pack();
table.act(Core.graphics.getDeltaTime());
addChild(table);
lastTable[0] = table;
});
}
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);
tools.stack(button, mode);
};
tools.defaults().size(size, size);
tools.addImageButton(Icon.menuLargeSmall, Styles.cleari, menu::show);
ImageButton grid = tools.addImageButton(Icon.gridSmall, Styles.clearTogglei, () -> view.setGrid(!view.isGrid())).get();
addTool.get(EditorTool.zoom);
tools.row();
ImageButton undo = tools.addImageButton(Icon.undoSmall, Styles.cleari, editor::undo).get();
ImageButton redo = tools.addImageButton(Icon.redoSmall, Styles.cleari, editor::redo).get();
addTool.get(EditorTool.pick);
tools.row();
undo.setDisabled(() -> !editor.canUndo());
redo.setDisabled(() -> !editor.canRedo());
undo.update(() -> undo.getImage().setColor(undo.isDisabled() ? Color.gray : Color.white));
redo.update(() -> redo.getImage().setColor(redo.isDisabled() ? Color.gray : Color.white));
grid.update(() -> grid.setChecked(view.isGrid()));
addTool.get(EditorTool.line);
addTool.get(EditorTool.pencil);
addTool.get(EditorTool.eraser);
tools.row();
addTool.get(EditorTool.fill);
addTool.get(EditorTool.spray);
ImageButton rotate = tools.addImageButton(Icon.arrow16Small, Styles.cleari, () -> editor.rotation = (editor.rotation + 1) % 4).get();
rotate.getImage().update(() -> {
rotate.getImage().setRotation(editor.rotation * 90);
rotate.getImage().setOrigin(Align.center);
});
tools.row();
tools.table(Tex.underline, t -> t.add("$editor.teams"))
.colspan(3).height(40).width(size * 3f + 3f).padBottom(3);
tools.row();
ButtonGroup<ImageButton> teamgroup = new ButtonGroup<>();
int i = 0;
for(Team team : Team.all){
ImageButton button = new ImageButton(Tex.whiteui, Styles.clearTogglePartiali);
button.margin(4f);
button.getImageCell().grow();
button.getStyle().imageUpColor = team.color;
button.clicked(() -> editor.drawTeam = team);
button.update(() -> button.setChecked(editor.drawTeam == team));
teamgroup.add(button);
tools.add(button);
if(i++ % 3 == 2) tools.row();
}
mid.add(tools).top().padBottom(-6);
mid.row();
mid.table(Tex.underline, t -> {
Slider slider = new Slider(0, MapEditor.brushSizes.length - 1, 1, false);
slider.moved(f -> editor.brushSize = MapEditor.brushSizes[(int)(float)f]);
for(int j = 0; j < MapEditor.brushSizes.length; j++){
if(MapEditor.brushSizes[j] == editor.brushSize){
slider.setValue(j);
}
}
t.top();
t.add("$editor.brush");
t.row();
t.add(slider).width(size * 3f - 20).padTop(4f);
}).padTop(5).growX().top();
}).margin(0).left().growY();
cont.table(t -> t.add(view).grow()).grow();
cont.table(this::addBlockSelection).right().growY();
}).grow();
}
private void doInput(){
if(Core.input.ctrl()){
//alt mode select
for(int i = 0; i < view.getTool().altModes.length + 1; i++){
if(Core.input.keyTap(KeyCode.valueOf("NUM_" + (i + 1)))){
view.getTool().mode = i - 1;
}
}
}else{
//tool select
for(int i = 0; i < EditorTool.values().length; i++){
if(Core.input.keyTap(KeyCode.valueOf("NUM_" + (i + 1)))){
view.setTool(EditorTool.values()[i]);
break;
}
}
}
if(Core.input.keyTap(KeyCode.ESCAPE)){
if(!menu.isShown()){
menu.show();
}
}
if(Core.input.keyTap(KeyCode.R)){
editor.rotation = Mathf.mod(editor.rotation + 1, 4);
}
if(Core.input.keyTap(KeyCode.E)){
editor.rotation = Mathf.mod(editor.rotation - 1, 4);
}
//ctrl keys (undo, redo, save)
if(Core.input.ctrl()){
if(Core.input.keyTap(KeyCode.Z)){
editor.undo();
}
//more undocumented features, fantastic
if(Core.input.keyTap(KeyCode.T)){
//clears all 'decoration' from the map
for(int x = 0; x < editor.width(); x++){
for(int y = 0; y < editor.height(); y++){
Tile tile = editor.tile(x, y);
if(tile.block().breakable && tile.block() instanceof Rock){
tile.setBlock(Blocks.air);
editor.renderer().updatePoint(x, y);
}
if(tile.overlay() != Blocks.air && tile.overlay() != Blocks.spawn){
tile.setOverlay(Blocks.air);
editor.renderer().updatePoint(x, y);
}
}
}
editor.flushOp();
}
if(Core.input.keyTap(KeyCode.Y)){
editor.redo();
}
if(Core.input.keyTap(KeyCode.S)){
save();
}
if(Core.input.keyTap(KeyCode.G)){
view.setGrid(!view.isGrid());
}
}
}
private void tryExit(){
if(!saved){
ui.showConfirm("$confirm", "$editor.unsaved", this::hide);
}else{
hide();
}
}
private void addBlockSelection(Table table){
Table content = new Table();
pane = new ScrollPane(content);
pane.setFadeScrollBars(false);
pane.setOverscroll(true, false);
pane.exited(() -> {
if(pane.hasScroll()){
Core.scene.setScrollFocus(view);
}
});
ButtonGroup<ImageButton> group = new ButtonGroup<>();
int i = 0;
blocksOut.clear();
blocksOut.addAll(Vars.content.blocks());
blocksOut.sort((b1, b2) -> {
int core = -Boolean.compare(b1 instanceof CoreBlock, b2 instanceof CoreBlock);
if(core != 0) return core;
int synth = Boolean.compare(b1.synthetic(), b2.synthetic());
if(synth != 0) return synth;
int ore = Boolean.compare(b1 instanceof OverlayFloor, b2 instanceof OverlayFloor);
if(ore != 0) return ore;
return Integer.compare(b1.id, b2.id);
});
for(Block block : blocksOut){
TextureRegion region = block.icon(Cicon.medium);
if(!Core.atlas.isFound(region)) continue;
ImageButton button = new ImageButton(Tex.whiteui, Styles.clearTogglei);
button.getStyle().imageUp = new TextureRegionDrawable(region);
button.clicked(() -> editor.drawBlock = block);
button.resizeImage(8 * 4f);
button.update(() -> button.setChecked(editor.drawBlock == block));
group.add(button);
content.add(button).size(50f);
if(++i % 4 == 0){
content.row();
}
}
group.getButtons().get(2).setChecked(true);
table.table(Tex.underline, extra -> extra.labelWrap(() -> editor.drawBlock.localizedName).width(200f).center()).growX();
table.row();
table.add(pane).growY().fillX();
}
}

View File

@@ -0,0 +1,444 @@
package mindustry.editor;
import arc.*;
import arc.struct.*;
import arc.func.*;
import arc.graphics.*;
import arc.graphics.Pixmap.*;
import arc.math.*;
import arc.math.geom.*;
import arc.scene.ui.*;
import arc.scene.ui.ImageButton.*;
import arc.scene.ui.layout.*;
import arc.util.*;
import arc.util.async.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.io.*;
import mindustry.maps.filters.*;
import mindustry.maps.filters.GenerateFilter.*;
import mindustry.ui.*;
import mindustry.ui.dialogs.*;
import mindustry.world.*;
import mindustry.world.blocks.*;
import static mindustry.Vars.*;
@SuppressWarnings("unchecked")
public class MapGenerateDialog extends FloatingDialog{
private final Prov<GenerateFilter>[] filterTypes = new Prov[]{
NoiseFilter::new, ScatterFilter::new, TerrainFilter::new, DistortFilter::new,
RiverNoiseFilter::new, OreFilter::new, OreMedianFilter::new, MedianFilter::new,
BlendFilter::new, MirrorFilter::new, ClearFilter::new
};
private final MapEditor editor;
private final boolean applied;
private Pixmap pixmap;
private Texture texture;
private GenerateInput input = new GenerateInput();
private Array<GenerateFilter> filters = new Array<>();
private int scaling = mobile ? 3 : 1;
private Table filterTable;
private AsyncExecutor executor = new AsyncExecutor(1);
private AsyncResult<Void> result;
private boolean generating;
private GenTile returnTile = new GenTile();
private GenTile[][] buffer1, buffer2;
private Cons<Array<GenerateFilter>> applier;
private CachedTile ctile = new CachedTile(){
//nothing.
@Override
protected void changed(){
}
};
/** @param applied whether or not to use the applied in-game mode. */
public MapGenerateDialog(MapEditor editor, boolean applied){
super("$editor.generate");
this.editor = editor;
this.applied = applied;
shown(this::setup);
addCloseButton();
if(applied){
buttons.addButton("$editor.apply", () -> {
ui.loadAnd(() -> {
apply();
hide();
});
}).size(160f, 64f);
}else{
buttons.addButton("$settings.reset", () -> {
filters.set(maps.readFilters(""));
rebuildFilters();
update();
}).size(160f, 64f);
}
buttons.addButton("$editor.randomize", () -> {
for(GenerateFilter filter : filters){
filter.randomize();
}
update();
}).size(160f, 64f);
buttons.addImageTextButton("$add", Icon.add, this::showAdd).height(64f).width(140f);
if(!applied){
hidden(this::apply);
}
onResize(this::rebuildFilters);
}
public void show(Array<GenerateFilter> filters, Cons<Array<GenerateFilter>> applier){
this.filters = filters;
this.applier = applier;
show();
}
public void show(Cons<Array<GenerateFilter>> applier){
show(this.filters, applier);
}
/** Applies the specified filters to the editor. */
public void applyToEditor(Array<GenerateFilter> filters){
//writeback buffer
GenTile[][] writeTiles = new GenTile[editor.width()][editor.height()];
for(int x = 0; x < editor.width(); x++){
for(int y = 0; y < editor.height(); y++){
writeTiles[x][y] = new GenTile();
}
}
for(GenerateFilter filter : filters){
input.begin(filter, editor.width(), editor.height(), editor::tile);
//write to buffer
for(int x = 0; x < editor.width(); x++){
for(int y = 0; y < editor.height(); y++){
Tile tile = editor.tile(x, y);
input.apply(x, y, tile.floor(), tile.block(), tile.overlay());
filter.apply(input);
writeTiles[x][y].set(input.floor, input.block, input.ore, tile.getTeam(), tile.rotation());
}
}
editor.load(() -> {
//read from buffer back into tiles
for(int x = 0; x < editor.width(); x++){
for(int y = 0; y < editor.height(); y++){
Tile tile = editor.tile(x, y);
GenTile write = writeTiles[x][y];
tile.rotation(write.rotation);
tile.setFloor((Floor)content.block(write.floor));
tile.setBlock(content.block(write.block));
tile.setTeam(Team.all[write.team]);
tile.setOverlay(content.block(write.ore));
}
}
});
}
//reset undo stack as generation... messes things up
editor.load(editor::checkLinkedTiles);
editor.renderer().updateAll();
editor.clearOp();
}
void setup(){
if(pixmap != null){
pixmap.dispose();
texture.dispose();
pixmap = null;
texture = null;
}
pixmap = new Pixmap(editor.width() / scaling, editor.height() / scaling, Format.RGBA8888);
texture = new Texture(pixmap);
cont.clear();
cont.table(t -> {
t.margin(8f);
t.stack(new BorderImage(texture){
{
setScaling(Scaling.fit);
}
@Override
public void draw(){
super.draw();
for(GenerateFilter filter : filters){
filter.draw(this);
}
}
}, new Stack(){{
add(new Image(Styles.black8));
add(new Image(Icon.refresh, Scaling.none));
visible(() -> generating && !updateEditorOnChange);
}}).grow().padRight(10);
t.pane(p -> filterTable = p.marginRight(6)).update(pane -> {
if(Core.scene.getKeyboardFocus() instanceof Dialog && Core.scene.getKeyboardFocus() != this){
return;
}
Vector2 v = pane.stageToLocalCoordinates(Core.input.mouse());
if(v.x >= 0 && v.y >= 0 && v.x <= pane.getWidth() && v.y <= pane.getHeight()){
Core.scene.setScrollFocus(pane);
}else{
Core.scene.setScrollFocus(null);
}
}).grow().get().setScrollingDisabled(true, false);
}).grow();
buffer1 = create();
buffer2 = create();
update();
rebuildFilters();
}
GenTile[][] create(){
GenTile[][] out = new GenTile[editor.width() / scaling][editor.height() / scaling];
for(int x = 0; x < out.length; x++){
for(int y = 0; y < out[0].length; y++){
out[x][y] = new GenTile();
}
}
return out;
}
void rebuildFilters(){
int cols = Math.max((int)(Math.max(filterTable.getParent().getWidth(), Core.graphics.getWidth()/2f * 0.9f) / Scl.scl(290f)), 1);
filterTable.clearChildren();
filterTable.top().left();
int i = 0;
for(GenerateFilter filter : filters){
//main container
filterTable.table(Tex.button, c -> {
//icons to perform actions
c.table(t -> {
t.top();
t.add(filter.name()).padTop(5).color(Pal.accent).growX().left();
t.row();
t.table(b -> {
ImageButtonStyle style = Styles.cleari;
b.defaults().size(50f);
b.addImageButton(Icon.refreshSmall, style, () -> {
filter.randomize();
update();
});
b.addImageButton(Icon.arrowUpSmall, style, () -> {
int idx = filters.indexOf(filter);
filters.swap(idx, Math.max(0, idx - 1));
rebuildFilters();
update();
});
b.addImageButton(Icon.arrowDownSmall, style, () -> {
int idx = filters.indexOf(filter);
filters.swap(idx, Math.min(filters.size - 1, idx + 1));
rebuildFilters();
update();
});
b.addImageButton(Icon.trashSmall, style, () -> {
filters.remove(filter);
rebuildFilters();
update();
});
});
}).fillX();
c.row();
//all the options
c.table(f -> {
f.left().top();
for(FilterOption option : filter.options){
option.changed = this::update;
f.table(t -> {
t.left();
option.build(t);
}).growX().left();
f.row();
}
}).grow().left().pad(2).top();
}).width(280f).pad(3).top().left().fillY();
if(++i % cols == 0){
filterTable.row();
}
}
if(filters.isEmpty()){
filterTable.add("$filters.empty").wrap().width(200f);
}
}
void showAdd(){
FloatingDialog selection = new FloatingDialog("$add");
selection.setFillParent(false);
selection.cont.defaults().size(210f, 60f);
int i = 0;
for(Prov<GenerateFilter> gen : filterTypes){
GenerateFilter filter = gen.get();
if(!applied && filter.buffered) continue;
selection.cont.addButton(filter.name(), () -> {
filters.add(filter);
rebuildFilters();
update();
selection.hide();
});
if(++i % 2 == 0) selection.cont.row();
}
selection.cont.addButton("$filter.defaultores", () -> {
maps.addDefaultOres(filters);
rebuildFilters();
update();
selection.hide();
});
selection.addCloseButton();
selection.show();
}
GenTile dset(Tile tile){
returnTile.set(tile);
return returnTile;
}
void apply(){
if(result != null){
result.get();
}
buffer1 = null;
buffer2 = null;
generating = false;
if(pixmap != null){
pixmap.dispose();
texture.dispose();
pixmap = null;
texture = null;
}
applier.get(filters);
}
void update(){
if(generating){
return;
}
Array<GenerateFilter> copy = new Array<>(filters);
result = executor.submit(() -> {
try{
generating = true;
if(!filters.isEmpty()){
//write to buffer1 for reading
for(int px = 0; px < pixmap.getWidth(); px++){
for(int py = 0; py < pixmap.getHeight(); py++){
buffer1[px][py].set(editor.tile(px * scaling, py * scaling));
}
}
}
for(GenerateFilter filter : copy){
input.begin(filter, editor.width(), editor.height(), (x, y) -> buffer1[Mathf.clamp(x / scaling, 0, pixmap.getWidth()-1)][Mathf.clamp(y / scaling, 0, pixmap.getHeight()-1)].tile());
//read from buffer1 and write to buffer2
for(int px = 0; px < pixmap.getWidth(); px++){
for(int py = 0; py < pixmap.getHeight(); py++){
int x = px * scaling, y = py * scaling;
GenTile tile = buffer1[px][py];
input.apply(x, y, content.block(tile.floor), content.block(tile.block), content.block(tile.ore));
filter.apply(input);
buffer2[px][py].set(input.floor, input.block, input.ore, Team.all[tile.team], tile.rotation);
}
}
for(int px = 0; px < pixmap.getWidth(); px++){
for(int py = 0; py < pixmap.getHeight(); py++){
buffer1[px][py].set(buffer2[px][py]);
}
}
}
for(int px = 0; px < pixmap.getWidth(); px++){
for(int py = 0; py < pixmap.getHeight(); py++){
int color;
//get result from buffer1 if there's filters left, otherwise get from editor directly
if(filters.isEmpty()){
Tile tile = editor.tile(px * scaling, py * scaling);
color = MapIO.colorFor(tile.floor(), tile.block(), tile.overlay(), Team.derelict);
}else{
GenTile tile = buffer1[px][py];
color = MapIO.colorFor(content.block(tile.floor), content.block(tile.block), content.block(tile.ore), Team.derelict);
}
pixmap.draw(px, pixmap.getHeight() - 1 - py, color);
}
}
Core.app.post(() -> {
if(pixmap == null || texture == null){
return;
}
texture.draw(pixmap, 0, 0);
generating = false;
});
}catch(Exception e){
generating = false;
e.printStackTrace();
}
return null;
});
}
private class GenTile{
public byte team, rotation;
public short block, floor, ore;
public void set(Block floor, Block wall, Block ore, Team team, int rotation){
this.floor = floor.id;
this.block = wall.id;
this.ore = ore.id;
this.team = (byte)team.ordinal();
this.rotation = (byte)rotation;
}
public void set(GenTile other){
this.floor = other.floor;
this.block = other.block;
this.ore = other.ore;
this.team = other.team;
this.rotation = other.rotation;
}
public GenTile set(Tile other){
set(other.floor(), other.block(), other.overlay(), other.getTeam(), other.rotation());
return this;
}
Tile tile(){
ctile.setFloor((Floor)content.block(floor));
ctile.setBlock(content.block(block));
ctile.setOverlay(content.block(ore));
ctile.rotation(rotation);
ctile.setTeam(Team.all[team]);
return ctile;
}
}
}

View File

@@ -0,0 +1,92 @@
package mindustry.editor;
import arc.*;
import arc.struct.*;
import arc.scene.ui.*;
import mindustry.*;
import mindustry.game.*;
import mindustry.io.*;
import mindustry.ui.*;
import mindustry.ui.dialogs.*;
public class MapInfoDialog extends FloatingDialog{
private final MapEditor editor;
private final WaveInfoDialog waveInfo;
private final MapGenerateDialog generate;
private final CustomRulesDialog ruleInfo = new CustomRulesDialog();
public MapInfoDialog(MapEditor editor){
super("$editor.mapinfo");
this.editor = editor;
this.waveInfo = new WaveInfoDialog(editor);
this.generate = new MapGenerateDialog(editor, false);
addCloseButton();
shown(this::setup);
}
private void setup(){
cont.clear();
ObjectMap<String, String> tags = editor.getTags();
cont.pane(t -> {
t.add("$editor.name").padRight(8).left();
t.defaults().padTop(15);
TextField name = t.addField(tags.get("name", ""), text -> {
tags.put("name", text);
}).size(400, 55f).get();
name.setMessageText("$unknown");
t.row();
t.add("$editor.description").padRight(8).left();
TextArea description = t.addArea(tags.get("description", ""), Styles.areaField, text -> {
tags.put("description", text);
}).size(400f, 140f).get();
t.row();
t.add("$editor.author").padRight(8).left();
TextField author = t.addField(tags.get("author", Core.settings.getString("mapAuthor", "")), text -> {
tags.put("author", text);
Core.settings.put("mapAuthor", text);
Core.settings.save();
}).size(400, 55f).get();
author.setMessageText("$unknown");
t.row();
t.add("$editor.rules").padRight(8).left();
t.addButton("$edit", () -> {
ruleInfo.show(Vars.state.rules, () -> Vars.state.rules = new Rules());
hide();
}).left().width(200f);
t.row();
t.add("$editor.waves").padRight(8).left();
t.addButton("$edit", () -> {
waveInfo.show();
hide();
}).left().width(200f);
t.row();
t.add("$editor.generation").padRight(8).left();
t.addButton("$edit", () -> {
generate.show(Vars.maps.readFilters(editor.getTags().get("genfilters", "")),
filters -> editor.getTags().put("genfilters", JsonIO.write(filters)));
hide();
}).left().width(200f);
name.change();
description.change();
author.change();
Vars.platform.addDialog(name, 50);
Vars.platform.addDialog(author, 50);
Vars.platform.addDialog(description, 1000);
t.margin(16f);
});
}
}

View File

@@ -0,0 +1,76 @@
package mindustry.editor;
import arc.func.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.util.*;
import mindustry.maps.*;
import mindustry.ui.*;
import mindustry.ui.dialogs.*;
import static mindustry.Vars.maps;
public class MapLoadDialog extends FloatingDialog{
private Map selected = null;
public MapLoadDialog(Cons<Map> loader){
super("$editor.loadmap");
shown(this::rebuild);
TextButton button = new TextButton("$load");
button.setDisabled(() -> selected == null);
button.clicked(() -> {
if(selected != null){
loader.get(selected);
hide();
}
});
buttons.defaults().size(200f, 50f);
buttons.addButton("$cancel", this::hide);
buttons.add(button);
}
public void rebuild(){
cont.clear();
if(maps.all().size > 0){
selected = maps.all().first();
}
ButtonGroup<TextButton> group = new ButtonGroup<>();
int maxcol = 3;
int i = 0;
Table table = new Table();
table.defaults().size(200f, 90f).pad(4f);
table.margin(10f);
ScrollPane pane = new ScrollPane(table, Styles.horizontalPane);
pane.setFadeScrollBars(false);
for(Map map : maps.all()){
TextButton button = new TextButton(map.name(), Styles.togglet);
button.add(new BorderImage(map.safeTexture(), 2f).setScaling(Scaling.fit)).size(16 * 4f);
button.getCells().reverse();
button.clicked(() -> selected = map);
button.getLabelCell().grow().left().padLeft(5f);
group.add(button);
table.add(button);
if(++i % maxcol == 0) table.row();
}
if(maps.all().size == 0){
table.add("$maps.none").center();
}else{
cont.add("$editor.loadmap");
}
cont.row();
cont.add(pane);
}
}

View File

@@ -0,0 +1,184 @@
package mindustry.editor;
import arc.*;
import arc.struct.IntSet;
import arc.struct.IntSet.IntSetIterator;
import arc.graphics.Color;
import arc.graphics.Texture;
import arc.graphics.g2d.Draw;
import arc.graphics.g2d.TextureRegion;
import arc.math.Mathf;
import arc.util.*;
import mindustry.content.Blocks;
import mindustry.game.EventType.*;
import mindustry.game.Team;
import mindustry.graphics.IndexedRenderer;
import mindustry.world.Block;
import mindustry.world.Tile;
import mindustry.world.blocks.BlockPart;
import static mindustry.Vars.tilesize;
public class MapRenderer implements Disposable{
private static final int chunkSize = 64;
private IndexedRenderer[][] chunks;
private IntSet updates = new IntSet();
private IntSet delayedUpdates = new IntSet();
private MapEditor editor;
private int width, height;
private Texture texture;
public MapRenderer(MapEditor editor){
this.editor = editor;
this.texture = Core.atlas.find("clear-editor").getTexture();
Events.on(ContentReloadEvent.class, e -> {
texture = Core.atlas.find("clear-editor").getTexture();
});
}
public void resize(int width, int height){
if(chunks != null){
for(int x = 0; x < chunks.length; x++){
for(int y = 0; y < chunks[0].length; y++){
chunks[x][y].dispose();
}
}
}
chunks = new IndexedRenderer[(int)Math.ceil((float)width / chunkSize)][(int)Math.ceil((float)height / chunkSize)];
for(int x = 0; x < chunks.length; x++){
for(int y = 0; y < chunks[0].length; y++){
chunks[x][y] = new IndexedRenderer(chunkSize * chunkSize * 2);
}
}
this.width = width;
this.height = height;
updateAll();
}
public void draw(float tx, float ty, float tw, float th){
Draw.flush();
IntSetIterator it = updates.iterator();
while(it.hasNext){
int i = it.next();
int x = i % width;
int y = i / width;
render(x, y);
}
updates.clear();
updates.addAll(delayedUpdates);
delayedUpdates.clear();
//????
if(chunks == null){
return;
}
for(int x = 0; x < chunks.length; x++){
for(int y = 0; y < chunks[0].length; y++){
IndexedRenderer mesh = chunks[x][y];
if(mesh == null){
continue;
}
mesh.getTransformMatrix().setToTranslation(tx, ty).scale(tw / (width * tilesize), th / (height * tilesize));
mesh.setProjectionMatrix(Draw.proj());
mesh.render(texture);
}
}
}
public void updatePoint(int x, int y){
updates.add(x + y * width);
}
public void updateAll(){
for(int x = 0; x < width; x++){
for(int y = 0; y < height; y++){
render(x, y);
}
}
}
private void render(int wx, int wy){
int x = wx / chunkSize, y = wy / chunkSize;
IndexedRenderer mesh = chunks[x][y];
Tile tile = editor.tiles()[wx][wy];
Team team = tile.getTeam();
Block floor = tile.floor();
Block wall = tile.block();
TextureRegion region;
int idxWall = (wx % chunkSize) + (wy % chunkSize) * chunkSize;
int idxDecal = (wx % chunkSize) + (wy % chunkSize) * chunkSize + chunkSize * chunkSize;
if(wall != Blocks.air && (wall.synthetic() || wall instanceof BlockPart)){
region = !Core.atlas.isFound(wall.editorIcon()) ? Core.atlas.find("clear-editor") : wall.editorIcon();
if(wall.rotate){
mesh.draw(idxWall, region,
wx * tilesize + wall.offset(), wy * tilesize + wall.offset(),
region.getWidth() * Draw.scl, region.getHeight() * Draw.scl, tile.rotation() * 90 - 90);
}else{
float width = region.getWidth() * Draw.scl, height = region.getHeight() * Draw.scl;
mesh.draw(idxWall, region,
wx * tilesize + wall.offset() + (tilesize - width) / 2f,
wy * tilesize + wall.offset() + (tilesize - height) / 2f,
width, height);
}
}else{
region = floor.editorVariantRegions()[Mathf.randomSeed(idxWall, 0, floor.editorVariantRegions().length - 1)];
mesh.draw(idxWall, region, wx * tilesize, wy * tilesize, 8, 8);
}
float offsetX = -(wall.size / 3) * tilesize, offsetY = -(wall.size / 3) * tilesize;
if(wall.update || wall.destructible){
mesh.setColor(team.color);
region = Core.atlas.find("block-border-editor");
}else if(!wall.synthetic() && wall != Blocks.air){
region = !Core.atlas.isFound(wall.editorIcon()) ? Core.atlas.find("clear-editor") : wall.editorIcon();
offsetX = tilesize / 2f - region.getWidth() / 2f * Draw.scl;
offsetY = tilesize / 2f - region.getHeight() / 2f * Draw.scl;
}else if(wall == Blocks.air && tile.overlay() != null){
region = tile.overlay().editorVariantRegions()[Mathf.randomSeed(idxWall, 0, tile.overlay().editorVariantRegions().length - 1)];
}else{
region = Core.atlas.find("clear-editor");
}
float width = region.getWidth() * Draw.scl, height = region.getHeight() * Draw.scl;
if(!wall.synthetic() && wall != Blocks.air && !wall.isMultiblock()){
offsetX = 0;
offsetY = 0;
width = tilesize;
height = tilesize;
}
mesh.draw(idxDecal, region, wx * tilesize + offsetX, wy * tilesize + offsetY, width, height);
mesh.setColor(Color.white);
}
@Override
public void dispose(){
if(chunks == null){
return;
}
for(int x = 0; x < chunks.length; x++){
for(int y = 0; y < chunks[0].length; y++){
if(chunks[x][y] != null){
chunks[x][y].dispose();
}
}
}
}
}

View File

@@ -0,0 +1,58 @@
package mindustry.editor;
import arc.func.*;
import arc.math.*;
import arc.scene.ui.layout.*;
import mindustry.gen.*;
import mindustry.ui.dialogs.*;
public class MapResizeDialog extends FloatingDialog{
private static final int minSize = 50, maxSize = 500, increment = 50;
int width, height;
public MapResizeDialog(MapEditor editor, Intc2 cons){
super("$editor.resizemap");
shown(() -> {
cont.clear();
width = editor.width();
height = editor.height();
Table table = new Table();
for(boolean w : Mathf.booleans){
table.add(w ? "$width" : "$height").padRight(8f);
table.defaults().height(60f).padTop(8);
table.addButton("<", () -> {
if(w)
width = move(width, -1);
else
height = move(height, -1);
}).size(60f);
table.table(Tex.button, t -> t.label(() -> (w ? width : height) + "")).width(200);
table.addButton(">", () -> {
if(w)
width = move(width, 1);
else
height = move(height, 1);
}).size(60f);
table.row();
}
cont.row();
cont.add(table);
});
buttons.defaults().size(200f, 50f);
buttons.addButton("$cancel", this::hide);
buttons.addButton("$ok", () -> {
cons.get(width, height);
hide();
});
}
static int move(int value, int direction){
return Mathf.clamp((value / increment + direction) * increment, minSize, maxSize);
}
}

View File

@@ -0,0 +1,73 @@
package mindustry.editor;
import arc.func.*;
import arc.scene.ui.*;
import mindustry.*;
import mindustry.maps.*;
import mindustry.ui.dialogs.*;
import static mindustry.Vars.ui;
public class MapSaveDialog extends FloatingDialog{
private TextField field;
private Cons<String> listener;
public MapSaveDialog(Cons<String> cons){
super("$editor.savemap");
field = new TextField();
listener = cons;
Vars.platform.addDialog(field);
shown(() -> {
cont.clear();
cont.label(() -> {
Map map = Vars.maps.byName(field.getText());
if(map != null){
if(map.custom){
return "$editor.overwrite";
}else{
return "$editor.failoverwrite";
}
}
return "";
}).colspan(2);
cont.row();
cont.add("$editor.mapname").padRight(14f);
cont.add(field).size(220f, 48f);
});
buttons.defaults().size(200f, 50f).pad(2f);
buttons.addButton("$cancel", this::hide);
TextButton button = new TextButton("$save");
button.clicked(() -> {
if(!invalid()){
cons.get(field.getText());
hide();
}
});
button.setDisabled(this::invalid);
buttons.add(button);
}
public void save(){
if(!invalid()){
listener.get(field.getText());
}else{
ui.showErrorMessage("$editor.failoverwrite");
}
}
public void setFieldText(String text){
field.setText(text);
}
private boolean invalid(){
if(field.getText().isEmpty()){
return true;
}
Map map = Vars.maps.byName(field.getText());
return map != null && !map.custom;
}
}

View File

@@ -0,0 +1,343 @@
package mindustry.editor;
import arc.Core;
import arc.graphics.Color;
import arc.graphics.g2d.*;
import arc.input.GestureDetector;
import arc.input.GestureDetector.GestureListener;
import arc.input.KeyCode;
import arc.math.Mathf;
import arc.math.geom.*;
import arc.scene.Element;
import arc.scene.event.*;
import arc.scene.ui.TextField;
import arc.scene.ui.layout.Scl;
import arc.util.*;
import mindustry.graphics.Pal;
import mindustry.input.Binding;
import mindustry.ui.GridImage;
import static mindustry.Vars.mobile;
import static mindustry.Vars.ui;
public class MapView extends Element implements GestureListener{
private MapEditor editor;
private EditorTool tool = EditorTool.pencil;
private float offsetx, offsety;
private float zoom = 1f;
private boolean grid = false;
private GridImage image = new GridImage(0, 0);
private Vector2 vec = new Vector2();
private Rectangle rect = new Rectangle();
private Vector2[][] brushPolygons = new Vector2[MapEditor.brushSizes.length][0];
private boolean drawing;
private int lastx, lasty;
private int startx, starty;
private float mousex, mousey;
private EditorTool lastTool;
public MapView(MapEditor editor){
this.editor = editor;
for(int i = 0; i < MapEditor.brushSizes.length; i++){
float size = MapEditor.brushSizes[i];
brushPolygons[i] = Geometry.pixelCircle(size, (index, x, y) -> Mathf.dst(x, y, index, index) <= index - 0.5f);
}
Core.input.getInputProcessors().insert(0, new GestureDetector(20, 0.5f, 2, 0.15f, this));
touchable(Touchable.enabled);
Point2 firstTouch = new Point2();
addListener(new InputListener(){
@Override
public boolean mouseMoved(InputEvent event, float x, float y){
mousex = x;
mousey = y;
requestScroll();
return false;
}
@Override
public void enter(InputEvent event, float x, float y, int pointer, Element fromActor){
requestScroll();
}
@Override
public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){
if(pointer != 0){
return false;
}
if(!mobile && button != KeyCode.MOUSE_LEFT && button != KeyCode.MOUSE_MIDDLE){
return true;
}
if(button == KeyCode.MOUSE_MIDDLE){
lastTool = tool;
tool = EditorTool.zoom;
}
mousex = x;
mousey = y;
Point2 p = project(x, y);
lastx = p.x;
lasty = p.y;
startx = p.x;
starty = p.y;
tool.touched(editor, p.x, p.y);
firstTouch.set(p);
if(tool.edit){
ui.editor.resetSaved();
}
drawing = true;
return true;
}
@Override
public void touchUp(InputEvent event, float x, float y, int pointer, KeyCode button){
if(!mobile && button != KeyCode.MOUSE_LEFT && button != KeyCode.MOUSE_MIDDLE){
return;
}
drawing = false;
Point2 p = project(x, y);
if(tool == EditorTool.line){
ui.editor.resetSaved();
tool.touchedLine(editor, startx, starty, p.x, p.y);
}
editor.flushOp();
if(button == KeyCode.MOUSE_MIDDLE && lastTool != null){
tool = lastTool;
lastTool = null;
}
}
@Override
public void touchDragged(InputEvent event, float x, float y, int pointer){
mousex = x;
mousey = y;
Point2 p = project(x, y);
if(drawing && tool.draggable && !(p.x == lastx && p.y == lasty)){
ui.editor.resetSaved();
Bresenham2.line(lastx, lasty, p.x, p.y, (cx, cy) -> tool.touched(editor, cx, cy));
}
if(tool == EditorTool.line && tool.mode == 1){
if(Math.abs(p.x - firstTouch.x) > Math.abs(p.y - firstTouch.y)){
lastx = p.x;
lasty = firstTouch.y;
}else{
lastx = firstTouch.x;
lasty = p.y;
}
}else{
lastx = p.x;
lasty = p.y;
}
}
});
}
public EditorTool getTool(){
return tool;
}
public void setTool(EditorTool tool){
this.tool = tool;
}
public boolean isGrid(){
return grid;
}
public void setGrid(boolean grid){
this.grid = grid;
}
@Override
public void act(float delta){
super.act(delta);
if(Core.scene.getKeyboardFocus() == null || !(Core.scene.getKeyboardFocus() instanceof TextField) && !Core.input.keyDown(KeyCode.CONTROL_LEFT)){
float ax = Core.input.axis(Binding.move_x);
float ay = Core.input.axis(Binding.move_y);
offsetx -= ax * 15f / zoom;
offsety -= ay * 15f / zoom;
}
if(Core.input.keyTap(KeyCode.SHIFT_LEFT)){
lastTool = tool;
tool = EditorTool.pick;
}
if(Core.input.keyRelease(KeyCode.SHIFT_LEFT) && lastTool != null){
tool = lastTool;
lastTool = null;
}
if(ui.editor.hasPane()) return;
zoom += Core.input.axis(KeyCode.SCROLL) / 10f * zoom;
clampZoom();
}
private void clampZoom(){
zoom = Mathf.clamp(zoom, 0.2f, 20f);
}
private Point2 project(float x, float y){
float ratio = 1f / ((float)editor.width() / editor.height());
float size = Math.min(width, height);
float sclwidth = size * zoom;
float sclheight = size * zoom * ratio;
x = (x - getWidth() / 2 + sclwidth / 2 - offsetx * zoom) / sclwidth * editor.width();
y = (y - getHeight() / 2 + sclheight / 2 - offsety * zoom) / sclheight * editor.height();
if(editor.drawBlock.size % 2 == 0 && tool != EditorTool.eraser){
return Tmp.g1.set((int)(x - 0.5f), (int)(y - 0.5f));
}else{
return Tmp.g1.set((int)x, (int)y);
}
}
private Vector2 unproject(int x, int y){
float ratio = 1f / ((float)editor.width() / editor.height());
float size = Math.min(width, height);
float sclwidth = size * zoom;
float sclheight = size * zoom * ratio;
float px = ((float)x / editor.width()) * sclwidth + offsetx * zoom - sclwidth / 2 + getWidth() / 2;
float py = ((float)(y) / editor.height()) * sclheight
+ offsety * zoom - sclheight / 2 + getHeight() / 2;
return vec.set(px, py);
}
@Override
public void draw(){
float ratio = 1f / ((float)editor.width() / editor.height());
float size = Math.min(width, height);
float sclwidth = size * zoom;
float sclheight = size * zoom * ratio;
float centerx = x + width / 2 + offsetx * zoom;
float centery = y + height / 2 + offsety * zoom;
image.setImageSize(editor.width(), editor.height());
if(!ScissorStack.pushScissors(rect.set(x, y, width, height))){
return;
}
Draw.color(Pal.remove);
Lines.stroke(2f);
Lines.rect(centerx - sclwidth / 2 - 1, centery - sclheight / 2 - 1, sclwidth + 2, sclheight + 2);
editor.renderer().draw(centerx - sclwidth / 2, centery - sclheight / 2, sclwidth, sclheight);
Draw.reset();
if(grid){
Draw.color(Color.gray);
image.setBounds(centerx - sclwidth / 2, centery - sclheight / 2, sclwidth, sclheight);
image.draw();
Draw.color();
}
int index = 0;
for(int i = 0; i < MapEditor.brushSizes.length; i++){
if(editor.brushSize == MapEditor.brushSizes[i]){
index = i;
break;
}
}
float scaling = zoom * Math.min(width, height) / editor.width();
Draw.color(Pal.accent);
Lines.stroke(Scl.scl(2f));
if((!editor.drawBlock.isMultiblock() || tool == EditorTool.eraser) && tool != EditorTool.fill){
if(tool == EditorTool.line && drawing){
Vector2 v1 = unproject(startx, starty).add(x, y);
float sx = v1.x, sy = v1.y;
Vector2 v2 = unproject(lastx, lasty).add(x, y);
Lines.poly(brushPolygons[index], sx, sy, scaling);
Lines.poly(brushPolygons[index], v2.x, v2.y, scaling);
}
if((tool.edit || (tool == EditorTool.line && !drawing)) && (!mobile || drawing)){
Point2 p = project(mousex, mousey);
Vector2 v = unproject(p.x, p.y).add(x, y);
//pencil square outline
if(tool == EditorTool.pencil && tool.mode == 1){
Lines.square(v.x + scaling/2f, v.y + scaling/2f, scaling * (editor.brushSize + 0.5f));
}else{
Lines.poly(brushPolygons[index], v.x, v.y, scaling);
}
}
}else{
if((tool.edit || tool == EditorTool.line) && (!mobile || drawing)){
Point2 p = project(mousex, mousey);
Vector2 v = unproject(p.x, p.y).add(x, y);
float offset = (editor.drawBlock.size % 2 == 0 ? scaling / 2f : 0f);
Lines.square(
v.x + scaling / 2f + offset,
v.y + scaling / 2f + offset,
scaling * editor.drawBlock.size / 2f);
}
}
Draw.color(Pal.accent);
Lines.stroke(Scl.scl(3f));
Lines.rect(x, y, width, height);
Draw.reset();
ScissorStack.popScissors();
}
private boolean active(){
return Core.scene.getKeyboardFocus() != null
&& Core.scene.getKeyboardFocus().isDescendantOf(ui.editor)
&& ui.editor.isShown() && tool == EditorTool.zoom &&
Core.scene.hit(Core.input.mouse().x, Core.input.mouse().y, true) == this;
}
@Override
public boolean pan(float x, float y, float deltaX, float deltaY){
if(!active()) return false;
offsetx += deltaX / zoom;
offsety += deltaY / zoom;
return false;
}
@Override
public boolean zoom(float initialDistance, float distance){
if(!active()) return false;
float nzoom = distance - initialDistance;
zoom += nzoom / 10000f / Scl.scl(1f) * zoom;
clampZoom();
return false;
}
@Override
public boolean pinch(Vector2 initialPointer1, Vector2 initialPointer2, Vector2 pointer1, Vector2 pointer2){
return false;
}
@Override
public void pinchStop(){
}
}

View File

@@ -0,0 +1,51 @@
package mindustry.editor;
import arc.struct.Array;
public class OperationStack{
private final static int maxSize = 10;
private Array<DrawOperation> stack = new Array<>();
private int index = 0;
public OperationStack(){
}
public void clear(){
stack.clear();
index = 0;
}
public void add(DrawOperation action){
stack.truncate(stack.size + index);
index = 0;
stack.add(action);
if(stack.size > maxSize){
stack.remove(0);
}
}
public boolean canUndo(){
return !(stack.size - 1 + index < 0);
}
public boolean canRedo(){
return !(index > -1 || stack.size + index < 0);
}
public void undo(){
if(!canUndo()) return;
stack.get(stack.size - 1 + index).undo();
index--;
}
public void redo(){
if(!canRedo()) return;
index++;
stack.get(stack.size - 1 + index).redo();
}
}

View File

@@ -0,0 +1,274 @@
package mindustry.editor;
import arc.*;
import arc.struct.*;
import arc.graphics.*;
import arc.input.*;
import arc.math.*;
import arc.scene.event.*;
import arc.scene.ui.*;
import arc.scene.ui.TextField.*;
import arc.scene.ui.layout.*;
import arc.util.*;
import mindustry.*;
import mindustry.content.*;
import mindustry.ctype.ContentType;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.io.*;
import mindustry.type.*;
import mindustry.ui.Cicon;
import mindustry.ui.dialogs.*;
import static mindustry.Vars.*;
import static mindustry.game.SpawnGroup.never;
public class WaveInfoDialog extends FloatingDialog{
private final static int displayed = 20;
private Array<SpawnGroup> groups = new Array<>();
private Table table, preview;
private int start = 0;
private UnitType lastType = UnitTypes.dagger;
private float updateTimer, updatePeriod = 1f;
public WaveInfoDialog(MapEditor editor){
super("$waves.title");
shown(this::setup);
hidden(() -> {
state.rules.spawns = groups;
});
keyDown(key -> {
if(key == KeyCode.ESCAPE || key == KeyCode.BACK){
Core.app.post(this::hide);
}
});
addCloseButton();
buttons.addButton("$waves.edit", () -> {
FloatingDialog dialog = new FloatingDialog("$waves.edit");
dialog.addCloseButton();
dialog.setFillParent(false);
dialog.cont.defaults().size(210f, 64f);
dialog.cont.addButton("$waves.copy", () -> {
ui.showInfoFade("$waves.copied");
Core.app.setClipboardText(maps.writeWaves(groups));
dialog.hide();
}).disabled(b -> groups == null);
dialog.cont.row();
dialog.cont.addButton("$waves.load", () -> {
try{
groups = maps.readWaves(Core.app.getClipboardText());
buildGroups();
}catch(Exception e){
e.printStackTrace();
ui.showErrorMessage("$waves.invalid");
}
dialog.hide();
}).disabled(b -> Core.app.getClipboardText() == null || Core.app.getClipboardText().isEmpty());
dialog.cont.row();
dialog.cont.addButton("$settings.reset", () -> ui.showConfirm("$confirm", "$settings.clear.confirm", () -> {
groups = JsonIO.copy(defaultWaves.get());
buildGroups();
dialog.hide();
}));
dialog.show();
}).size(270f, 64f);
}
void setup(){
groups = JsonIO.copy(state.rules.spawns.isEmpty() ? defaultWaves.get() : state.rules.spawns);
cont.clear();
cont.stack(new Table(Tex.clear, main -> {
main.pane(t -> table = t).growX().growY().padRight(8f).get().setScrollingDisabled(true, false);
main.row();
main.addButton("$add", () -> {
if(groups == null) groups = new Array<>();
groups.add(new SpawnGroup(lastType));
buildGroups();
}).growX().height(70f);
}), new Label("$waves.none"){{
visible(() -> groups.isEmpty());
touchable(Touchable.disabled);
setWrap(true);
setAlignment(Align.center, Align.center);
}}).width(390f).growY();
cont.table(Tex.clear, m -> {
m.add("$waves.preview").color(Color.lightGray).growX().center().get().setAlignment(Align.center, Align.center);
m.row();
m.addButton("-", () -> {
}).update(t -> {
if(t.getClickListener().isPressed()){
updateTimer += Time.delta();
if(updateTimer >= updatePeriod){
start = Math.max(start - 1, 0);
updateTimer = 0f;
updateWaves();
}
}
}).growX().height(70f);
m.row();
m.pane(t -> preview = t).grow().get().setScrollingDisabled(true, true);
m.row();
m.addButton("+", () -> {
}).update(t -> {
if(t.getClickListener().isPressed()){
updateTimer += Time.delta();
if(updateTimer >= updatePeriod){
start++;
updateTimer = 0f;
updateWaves();
}
}
}).growX().height(70f);
}).growY().width(180f).growY();
buildGroups();
}
void buildGroups(){
table.clear();
table.top();
table.margin(10f);
if(groups != null){
for(SpawnGroup group : groups){
table.table(Tex.button, t -> {
t.margin(0).defaults().pad(3).padLeft(5f).growX().left();
t.addButton(b -> {
b.left();
b.addImage(group.type.icon(mindustry.ui.Cicon.medium)).size(32f).padRight(3);
b.add(group.type.localizedName).color(Pal.accent);
}, () -> showUpdate(group)).pad(-6f).padBottom(0f);
t.row();
t.table(spawns -> {
spawns.addField("" + (group.begin + 1), TextFieldFilter.digitsOnly, text -> {
if(Strings.canParsePostiveInt(text)){
group.begin = Strings.parseInt(text) - 1;
updateWaves();
}
}).width(100f);
spawns.add("$waves.to").padLeft(4).padRight(4);
spawns.addField(group.end == never ? "" : (group.end + 1) + "", TextFieldFilter.digitsOnly, text -> {
if(Strings.canParsePostiveInt(text)){
group.end = Strings.parseInt(text) - 1;
updateWaves();
}else if(text.isEmpty()){
group.end = never;
updateWaves();
}
}).width(100f).get().setMessageText(Core.bundle.get("waves.never"));
});
t.row();
t.table(p -> {
p.add("$waves.every").padRight(4);
p.addField(group.spacing + "", TextFieldFilter.digitsOnly, text -> {
if(Strings.canParsePostiveInt(text) && Strings.parseInt(text) > 0){
group.spacing = Strings.parseInt(text);
updateWaves();
}
}).width(100f);
p.add("$waves.waves").padLeft(4);
});
t.row();
t.table(a -> {
a.addField(group.unitAmount + "", TextFieldFilter.digitsOnly, text -> {
if(Strings.canParsePostiveInt(text)){
group.unitAmount = Strings.parseInt(text);
updateWaves();
}
}).width(80f);
a.add(" + ");
a.addField(Strings.fixed(Math.max((Mathf.zero(group.unitScaling) ? 0 : 1f / group.unitScaling), 0), 2), TextFieldFilter.floatsOnly, text -> {
if(Strings.canParsePositiveFloat(text)){
group.unitScaling = 1f / Strings.parseFloat(text);
updateWaves();
}
}).width(80f);
a.add("$waves.perspawn").padLeft(4);
});
t.row();
t.addCheck("$waves.boss", b -> group.effect = (b ? StatusEffects.boss : null)).padTop(4).update(b -> b.setChecked(group.effect == StatusEffects.boss));
t.row();
t.addButton("$waves.remove", () -> {
groups.remove(group);
table.getCell(t).pad(0f);
t.remove();
updateWaves();
}).growX().pad(-6f).padTop(5);
}).width(340f).pad(16);
table.row();
}
}else{
table.add("$editor.default");
}
updateWaves();
}
void showUpdate(SpawnGroup group){
FloatingDialog dialog = new FloatingDialog("");
dialog.setFillParent(true);
dialog.cont.pane(p -> {
int i = 0;
for(UnitType type : content.units()){
p.addButton(t -> {
t.left();
t.addImage(type.icon(mindustry.ui.Cicon.medium)).size(40f).padRight(2f);
t.add(type.localizedName);
}, () -> {
lastType = type;
group.type = type;
dialog.hide();
buildGroups();
}).pad(2).margin(12f).fillX();
if(++i % 3 == 0) p.row();
}
});
dialog.show();
}
void updateWaves(){
preview.clear();
preview.top();
for(int i = start; i < displayed + start; i++){
int wave = i;
preview.table(Tex.underline, table -> {
table.add((wave + 1) + "").color(Pal.accent).center().colspan(2).get().setAlignment(Align.center, Align.center);
table.row();
int[] spawned = new int[Vars.content.getBy(ContentType.unit).size];
for(SpawnGroup spawn : groups){
spawned[spawn.type.id] += spawn.getUnitsSpawned(wave);
}
for(int j = 0; j < spawned.length; j++){
if(spawned[j] > 0){
UnitType type = content.getByID(ContentType.unit, j);
table.addImage(type.icon(Cicon.medium)).size(8f * 4f).padRight(4);
table.add(spawned[j] + "x").color(Color.lightGray).padRight(6);
table.row();
}
}
if(table.getChildren().size == 1){
table.add("$none").color(Pal.remove);
}
}).width(110f).pad(2f);
preview.row();
}
}
}