Tree-like objective node structure (#7152)

* overall structure

* overall layout

* field interpreter

* less bloated UI

* scroll pan't

* strip off the 'Marker' suffix

* e

* all (hopefully all...) interpreters finished.

* onset

* two, four

* i don't understand how icon mappings work.

* separate remover and indexer

* some cleanups

* untested mobile support

* contrib

* ok anuke

* fix conflicts 2

* hidden
This commit is contained in:
GlennFolker
2022-07-15 21:41:18 +07:00
committed by GitHub
parent 3b1c8baca9
commit 695c19d0b0
26 changed files with 1733 additions and 810 deletions

View File

@@ -77,7 +77,7 @@ public class MapInfoDialog extends BaseDialog{
r.row();
r.button("@editor.objectives", Icon.info, style, () -> {
objectives.show(state.rules.objectives);
objectives.show(state.rules.objectives.all, state.rules.objectives.all::set);
hide();
}).marginLeft(10f);

View File

@@ -0,0 +1,570 @@
package mindustry.editor;
import arc.*;
import arc.graphics.*;
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.ImageButton.*;
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 static final float unitSize = 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, 0f);
tilemap.y = Mathf.clamp(tilemap.y + deltaY, -bounds * unitSize + height, 0f);
}
@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{
protected final GridBits grid = new GridBits(bounds, bounds);
/** 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 - 1f) / unitSize), 0), minY = Math.max(Mathf.floor((y - 1f) / unitSize), 0),
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(2f);
Draw.color(Pal.gray, 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) ? Pal.accent : Pal.remove,
parentAlpha * (inPlaceBounds(tx, ty) ? 1f : Mathf.absin(3f, 1f))
);
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(
validMove(moving, tx, ty) ? Pal.accent : Pal.remove,
0.5f * parentAlpha * (inPlaceBounds(tx, ty) ? 1f : Mathf.absin(3f, 1f))
);
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);
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);
float dist = Math.abs(x1 - x2) / 2f;
Lines.curve(x1, y1, x1 + dist, y1, x2 - dist, y2, x2, y2, Math.max(4, (int) (Mathf.dst(x1, y1, x2, y2) / 4f)));
Draw.reset();
}
public boolean inPlaceBounds(int x, int y){
return Structs.inBounds(x, y, bounds - objWidth + 1, bounds - objHeight + 1);
}
public boolean validPlace(int x, int y){
if(!inPlaceBounds(x, y)) return false;
for(int tx = 0; tx < objWidth; tx++){
for(int ty = 0; ty < objHeight; ty++){
if(occupied(x + tx, y + ty)) return false;
}
}
return true;
}
public boolean validMove(ObjectiveTile tile, int newX, int newY){
if(!inPlaceBounds(newX, newY)) return false;
int x = tile.tx, y = tile.ty;
for(int tx = 0; tx < objWidth; tx++){
for(int ty = 0; ty < objHeight; ty++){
grid.set(x + tx, y + ty, false);
}
}
boolean valid = validPlace(newX, newY);
for(int tx = 0; tx < objWidth; tx++){
for(int ty = 0; ty < objHeight; ty++){
grid.set(x + tx, y + ty);
}
}
return valid;
}
public boolean occupied(int x, int y){
return grid.get(x, y);
}
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)) return false;
ObjectiveTile tile = new ObjectiveTile(obj, x, y);
tile.pack();
addChild(tile);
for(int tx = 0; tx < objWidth; tx++){
for(int ty = 0; ty < objHeight; ty++){
grid.set(x + tx, y + ty);
}
}
return true;
}
public boolean moveTile(ObjectiveTile tile, int newX, int newY){
if(!validMove(tile, newX, newY)) return false;
int x = tile.tx, y = tile.ty;
for(int tx = 0; tx < objWidth; tx++){
for(int ty = 0; ty < objHeight; ty++){
grid.set(x + tx, y + ty, false);
}
}
tile.pos(newX, newY);
x = newX;
y = newY;
for(int tx = 0; tx < objWidth; tx++){
for(int ty = 0; ty < objHeight; ty++){
grid.set(x + tx, y + ty);
}
}
return true;
}
public void removeTile(ObjectiveTile tile){
if(!tile.isDescendantOf(this)) return;
tile.remove();
int x = tile.tx, y = tile.ty;
for(int tx = 0; tx < objWidth; tx++){
for(int ty = 0; ty < objHeight; ty++){
grid.set(x + tx, y + ty, false);
}
}
}
public void clearTiles(){
clearChildren();
grid.clear();
}
@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);
add(new ImageButton(Icon.move, new ImageButtonStyle(){{
up = Tex.whiteui;
imageUpColor = Color.black;
}})).color(Pal.accent).height(unitSize).growX().get().addCaptureListener(mover = new Mover());
add(conChildren = new Connector(false)).size(unitSize);
row().table(Tex.buttonSelectTrans, t -> {
t.labelWrap(obj.typeName()).grow()
.color(Pal.accent).align(Align.left).padLeft(6f)
.ellipsis(true).get().setAlignment(Align.left);
t.table(b -> {
b.right().defaults().size(32f).pad((unitSize - 32f) / 2f - 4f);
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));
}).growY().fillX();
}).grow().colspan(3);
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.buttonEdgeDown1 : Tex.buttonEdgeDown3;
up = findParent ? Tex.buttonEdge1 : Tex.buttonEdge3;
over = findParent ? Tex.buttonEdgeOver1 : Tex.buttonEdgeOver3;
}});
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();
}
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));
}
}
}
}
}

File diff suppressed because it is too large Load Diff