Files
Mindustry/core/src/mindustry/editor/MapObjectivesCanvas.java
2023-01-08 10:24:55 -05:00

556 lines
20 KiB
Java

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<MapObjective> 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<ObjectiveTile> 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<MapObjective>)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));
}
}
}
}
}