package mindustry.editor; import arc.*; import arc.graphics.g2d.*; import arc.input.*; import arc.math.*; import arc.math.geom.*; import arc.scene.event.*; import arc.scene.ui.*; import arc.scene.ui.layout.*; import arc.struct.*; import arc.util.*; import mindustry.editor.MapObjectivesCanvas.ObjectiveTilemap.ObjectiveTile.*; import mindustry.editor.MapObjectivesDialog.*; import mindustry.game.MapObjectives.*; import mindustry.gen.*; import mindustry.graphics.*; import mindustry.ui.*; import mindustry.ui.dialogs.*; import static mindustry.Vars.*; @SuppressWarnings("unchecked") public class MapObjectivesCanvas extends WidgetGroup{ public static final int objWidth = 5, objHeight = 2, bounds = 100; public final float unitSize = Scl.scl(48f); public Seq objectives = new Seq<>(); public ObjectiveTilemap tilemap; protected MapObjective query; private boolean pressed; private long visualPressed; private int queryX = -objWidth, queryY = -objHeight; public MapObjectivesCanvas(){ setFillParent(true); addChild(tilemap = new ObjectiveTilemap()); addCaptureListener(new InputListener(){ @Override public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){ if(query != null && button == KeyCode.mouseRight){ stopQuery(); event.stop(); return true; }else{ return false; } } }); addCaptureListener(new ElementGestureListener(){ int pressPointer = -1; @Override public void pan(InputEvent event, float x, float y, float deltaX, float deltaY){ if(tilemap.moving != null || tilemap.connecting != null) return; tilemap.x = Mathf.clamp(tilemap.x + deltaX, -bounds * unitSize + width, bounds * unitSize); tilemap.y = Mathf.clamp(tilemap.y + deltaY, -bounds * unitSize + height, bounds * unitSize); } @Override public void tap(InputEvent event, float x, float y, int count, KeyCode button){ if(query == null) return; Vec2 pos = localToDescendantCoordinates(tilemap, Tmp.v1.set(x, y)); queryX = Mathf.round((pos.x - objWidth * unitSize / 2f) / unitSize); queryY = Mathf.floor((pos.y - unitSize) / unitSize); // In mobile, placing the query is done in a separate button. if(!mobile) placeQuery(); } @Override public void touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){ if(pressPointer != -1) return; pressPointer = pointer; pressed = true; visualPressed = Time.millis() + 100; } @Override public void touchUp(InputEvent event, float x, float y, int pointer, KeyCode button){ if(pointer == pressPointer){ pressPointer = -1; pressed = false; } } }); } public void clearObjectives(){ stopQuery(); tilemap.clearTiles(); } protected void stopQuery(){ if(query == null) return; query = null; Core.graphics.restoreCursor(); } public void query(MapObjective obj){ stopQuery(); query = obj; } public void placeQuery(){ if(isQuerying() && tilemap.createTile(queryX, queryY, query)){ objectives.add(query); stopQuery(); } } public boolean isQuerying(){ return query != null; } public boolean isVisualPressed(){ return pressed || visualPressed > Time.millis(); } public class ObjectiveTilemap extends WidgetGroup{ /** The connector button that is being pressed. */ protected @Nullable Connector connecting; /** The current tile that is being moved. */ protected @Nullable ObjectiveTile moving; public ObjectiveTilemap(){ setTransform(false); setSize(getPrefWidth(), getPrefHeight()); touchable(() -> isQuerying() ? Touchable.disabled : Touchable.childrenOnly); } @Override public void draw(){ validate(); int minX = Math.max(Mathf.floor((x - width - 1f) / unitSize), -bounds), minY = Math.max(Mathf.floor((y - height - 1f) / unitSize), -bounds), maxX = Math.min(Mathf.ceil((x + width + 1f) / unitSize), bounds), maxY = Math.min(Mathf.ceil((y + height + 1f) / unitSize), bounds); float progX = x % unitSize, progY = y % unitSize; Lines.stroke(3f); Draw.color(Pal.darkestGray, parentAlpha); for(int x = minX; x <= maxX; x++) Lines.line(progX + x * unitSize, minY * unitSize, progX + x * unitSize, maxY * unitSize); for(int y = minY; y <= maxY; y++) Lines.line(minX * unitSize, progY + y * unitSize, maxX * unitSize, progY + y * unitSize); if(isQuerying()){ int tx, ty; if(mobile){ tx = queryX; ty = queryY; }else{ Vec2 pos = screenToLocalCoordinates(Core.input.mouse()); tx = Mathf.round((pos.x - objWidth * unitSize / 2f) / unitSize); ty = Mathf.floor((pos.y - unitSize) / unitSize); } Lines.stroke(4f); Draw.color( isVisualPressed() ? Pal.metalGrayDark : validPlace(tx, ty, null) ? Pal.accent : Pal.remove, parentAlpha ); Lines.rect(x + tx * unitSize, y + ty * unitSize, objWidth * unitSize, objHeight * unitSize); } if(moving != null){ int tx, ty; float x = this.x + (tx = Mathf.round(moving.x / unitSize)) * unitSize; float y = this.y + (ty = Mathf.round(moving.y / unitSize)) * unitSize; Draw.color( validPlace(tx, ty, moving) ? Pal.accent : Pal.remove, 0.5f * parentAlpha ); Fill.crect(x, y, objWidth * unitSize, objHeight * unitSize); } Draw.reset(); super.draw(); Draw.reset(); Seq tiles = getChildren().as(); Connector conTarget = null; if(connecting != null){ Vec2 pos = connecting.localToAscendantCoordinates(this, Tmp.v1.set(connecting.pointX, connecting.pointY)); if(hit(pos.x, pos.y, true) instanceof Connector con && connecting.canConnectTo(con)) conTarget = con; } boolean removing = false; for(var tile : tiles){ for(var parent : tile.obj.parents){ var parentTile = tiles.find(t -> t.obj == parent); if(parentTile == null) continue; Connector conFrom = parentTile.conChildren, conTo = tile.conParent; if(conTarget != null && ( (connecting.findParent && connecting == conTo && conTarget == conFrom) || (!connecting.findParent && connecting == conFrom && conTarget == conTo) )){ removing = true; continue; } Vec2 from = conFrom.localToAscendantCoordinates(this, Tmp.v1.set(conFrom.getWidth() / 2f, conFrom.getHeight() / 2f)).add(x, y), to = conTo.localToAscendantCoordinates(this, Tmp.v2.set(conTo.getWidth() / 2f, conTo.getHeight() / 2f)).add(x, y); drawCurve(false, from.x, from.y, to.x, to.y); } } if(connecting != null){ Vec2 mouse = (conTarget == null ? connecting.localToAscendantCoordinates(this, Tmp.v1.set(connecting.pointX, connecting.pointY)) : conTarget.localToAscendantCoordinates(this, Tmp.v1.set(conTarget.getWidth() / 2f, conTarget.getHeight() / 2f)) ).add(x, y), anchor = connecting.localToAscendantCoordinates(this, Tmp.v2.set(connecting.getWidth() / 2f, connecting.getHeight() / 2f)).add(x, y); Vec2 from = connecting.findParent ? mouse : anchor, to = connecting.findParent ? anchor : mouse; drawCurve(removing, from.x, from.y, to.x, to.y); } Draw.reset(); } protected void drawCurve(boolean remove, float x1, float y1, float x2, float y2){ Lines.stroke(4f); Draw.color(remove ? Pal.remove : Pal.accent, parentAlpha); Fill.square(x1, y1, 8f, 45f); Fill.square(x2, y2, 8f, 45f); float dist = Math.abs(x1 - x2) / 2f; float cx1 = x1 + dist; float cx2 = x2 - dist; Lines.curve(x1, y1, cx1, y1, cx2, y2, x2, y2, Math.max(4, (int) (Mathf.dst(x1, y1, x2, y2) / 4f))); float progress = (Time.time % (60 * 4)) / (60 * 4); float t2 = progress * progress; float t3 = progress * t2; float t1 = 1 - progress; float t13 = t1 * t1 * t1; float kx1 = t13 * x1 + 3 * progress * t1 * t1 * cx1 + 3 * t2 * t1 * cx2 + t3 * x2; float ky1 = t13 *y1 + 3 * progress * t1 * t1 * y1 + 3 * t2 * t1 * y2 + t3 * y2; Fill.circle(kx1, ky1, 6f); Draw.reset(); } public boolean validPlace(int x, int y, @Nullable ObjectiveTile ignore){ Tmp.r1.set(x, y, objWidth, objHeight).grow(-0.001f); if(!Tmp.r2.setCentered(0, 0, bounds * 2, bounds * 2).contains(Tmp.r1)){ return false; } for(var other : children){ if(other instanceof ObjectiveTile tile && tile != ignore && Tmp.r2.set(tile.tx, tile.ty, objWidth, objHeight).overlaps(Tmp.r1)){ return false; } } return true; } public boolean createTile(MapObjective obj){ return createTile(obj.editorX, obj.editorY, obj); } public boolean createTile(int x, int y, MapObjective obj){ if(!validPlace(x, y, null)) return false; ObjectiveTile tile = new ObjectiveTile(obj, x, y); tile.pack(); addChild(tile); return true; } public boolean moveTile(ObjectiveTile tile, int newX, int newY){ if(!validPlace(newX, newY, tile)) return false; tile.pos(newX, newY); return true; } public void removeTile(ObjectiveTile tile){ if(!tile.isDescendantOf(this)) return; tile.remove(); } public void clearTiles(){ clearChildren(); } @Override public float getPrefWidth(){ return bounds * unitSize; } @Override public float getPrefHeight(){ return bounds * unitSize; } public class ObjectiveTile extends Table{ public final MapObjective obj; public int tx, ty; public final Mover mover; public final Connector conParent, conChildren; public ObjectiveTile(MapObjective obj, int x, int y){ this.obj = obj; setTransform(false); setClip(false); add(conParent = new Connector(true)).size(unitSize / Scl.scl(1f), unitSize * 2 / Scl.scl(1f)); table(Tex.whiteui, t -> { float pad = (unitSize / Scl.scl(1f) - 32f) / 2f - 4f; t.margin(pad); t.touchable(() -> Touchable.enabled); t.setColor(Pal.gray); t.labelWrap(obj.typeName()) .style(Styles.outlineLabel) .left().grow().get() .setAlignment(Align.left); t.row(); t.table(b -> { b.left().defaults().size(40f); b.button(Icon.pencilSmall, () -> { BaseDialog dialog = new BaseDialog("@editor.objectives"); dialog.cont.pane(Styles.noBarPane, list -> list.top().table(e -> { e.margin(0f); MapObjectivesDialog.getInterpreter((Class)obj.getClass()).build( e, obj.typeName(), new TypeInfo(obj.getClass()), null, null, null, () -> obj, res -> {} ); }).width(400f).fillY()).grow(); dialog.addCloseButton(); dialog.show(); }); b.button(Icon.trashSmall, () -> removeTile(this)); }).left().grow(); }).growX().height(unitSize / Scl.scl(1f) * 2).get().addCaptureListener(mover = new Mover()); add(conChildren = new Connector(false)).size(unitSize / Scl.scl(1f), unitSize / Scl.scl(1f) * 2); setSize(getPrefWidth(), getPrefHeight()); pos(x, y); } public void pos(int x, int y){ tx = obj.editorX = x; ty = obj.editorY = y; this.x = x * unitSize; this.y = y * unitSize; } @Override public float getPrefWidth(){ return objWidth * unitSize; } @Override public float getPrefHeight(){ return objHeight * unitSize; } @Override public boolean remove(){ if(super.remove()){ obj.parents.clear(); var it = objectives.iterator(); while(it.hasNext()){ var next = it.next(); if(next == obj){ it.remove(); }else{ next.parents.remove(obj); } } return true; }else{ return false; } } public class Mover extends InputListener{ public int prevX, prevY; public float lastX, lastY; @Override public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){ if(moving != null) return false; moving = ObjectiveTile.this; moving.toFront(); prevX = moving.tx; prevY = moving.ty; // Convert to world pos first because the button gets dragged too. Vec2 pos = event.listenerActor.localToStageCoordinates(Tmp.v1.set(x, y)); lastX = pos.x; lastY = pos.y; return true; } @Override public void touchDragged(InputEvent event, float x, float y, int pointer){ Vec2 pos = event.listenerActor.localToStageCoordinates(Tmp.v1.set(x, y)); moving.moveBy(pos.x - lastX, pos.y - lastY); lastX = pos.x; lastY = pos.y; } @Override public void touchUp(InputEvent event, float x, float y, int pointer, KeyCode button){ if(!moveTile(moving, Mathf.round(moving.x / unitSize), Mathf.round(moving.y / unitSize) )) moving.pos(prevX, prevY); moving = null; } } public class Connector extends Button{ public float pointX, pointY; public final boolean findParent; public Connector(boolean findParent){ super(new ButtonStyle(){{ down = findParent ? Tex.buttonSideLeftDown : Tex.buttonSideRightDown; up = findParent ? Tex.buttonSideLeft : Tex.buttonSideRight; over = findParent ? Tex.buttonSideLeftOver : Tex.buttonSideRightOver; }}); this.findParent = findParent; clearChildren(); addCaptureListener(new InputListener(){ int conPointer = -1; @Override public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){ if(conPointer != -1) return false; conPointer = pointer; if(connecting != null) return false; connecting = Connector.this; pointX = x; pointY = y; return true; } @Override public void touchDragged(InputEvent event, float x, float y, int pointer){ if(conPointer != pointer) return; pointX = x; pointY = y; } @Override public void touchUp(InputEvent event, float x, float y, int pointer, KeyCode button){ if(conPointer != pointer || connecting != Connector.this) return; conPointer = -1; Vec2 pos = Connector.this.localToAscendantCoordinates(ObjectiveTilemap.this, Tmp.v1.set(x, y)); if(ObjectiveTilemap.this.hit(pos.x, pos.y, true) instanceof Connector con && con.canConnectTo(Connector.this)){ if(findParent){ if(!obj.parents.remove(con.tile().obj)) obj.parents.add(con.tile().obj); }else{ if(!con.tile().obj.parents.remove(obj)) con.tile().obj.parents.add(obj); } } connecting = null; } }); } public boolean canConnectTo(Connector other){ return findParent != other.findParent && tile() != other.tile(); } @Override public void draw(){ super.draw(); float cx = x + width / 2f; float cy = y + height / 2f; // these are all magic numbers tweaked until they looked good in-game, don't mind them. Lines.stroke(3f, Pal.accent); if(findParent){ Lines.line(cx, cy + 9f, cx + 9f, cy); Lines.line(cx + 9f, cy, cx, cy - 9f); }else{ Lines.square(cx, cy, 9f, 45f); } } public ObjectiveTile tile(){ return ObjectiveTile.this; } @Override public boolean isPressed(){ return super.isPressed() || connecting == this; } @Override public boolean isOver(){ return super.isOver() && (connecting == null || connecting.canConnectTo(this)); } } } } }