diff --git a/build.gradle b/build.gradle index 57b1a96a39..0002e4296f 100644 --- a/build.gradle +++ b/build.gradle @@ -365,6 +365,13 @@ project(":core"){ } } + gradle.taskGraph.whenReady{ + //these are completely unnecessary + tasks.kaptGenerateStubsKotlin.onlyIf{ false } + tasks.compileKotlin.onlyIf{ false } + tasks.inspectClassesForKotlinIC.onlyIf{ false } + } + //comp** classes are only used for code generation jar{ exclude("mindustry/entities/comp/**") diff --git a/core/assets-raw/sprites/ui/button-edge-down-1.9.png b/core/assets-raw/sprites/ui/button-edge-down-1.9.png new file mode 100644 index 0000000000..14811612f7 Binary files /dev/null and b/core/assets-raw/sprites/ui/button-edge-down-1.9.png differ diff --git a/core/assets-raw/sprites/ui/button-edge-down-3.9.png b/core/assets-raw/sprites/ui/button-edge-down-3.9.png new file mode 100644 index 0000000000..0bdf193a97 Binary files /dev/null and b/core/assets-raw/sprites/ui/button-edge-down-3.9.png differ diff --git a/core/assets-raw/sprites/ui/button-edge-over-1.9.png b/core/assets-raw/sprites/ui/button-edge-over-1.9.png new file mode 100644 index 0000000000..11e9d35f69 Binary files /dev/null and b/core/assets-raw/sprites/ui/button-edge-over-1.9.png differ diff --git a/core/assets-raw/sprites/ui/button-edge-over-3.9.png b/core/assets-raw/sprites/ui/button-edge-over-3.9.png new file mode 100644 index 0000000000..5137b12671 Binary files /dev/null and b/core/assets-raw/sprites/ui/button-edge-over-3.9.png differ diff --git a/core/assets-raw/sprites/ui/button-select-trans.9.png b/core/assets-raw/sprites/ui/button-select-trans.9.png new file mode 100644 index 0000000000..65c60f4aff Binary files /dev/null and b/core/assets-raw/sprites/ui/button-select-trans.9.png differ diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index 4089379032..b895e0687f 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -586,6 +586,28 @@ map.multiplayer = Only the host can view sectors. uncover = Uncover configure = Configure Loadout +objective.research.name = Research +objective.produce.name = Obtain +objective.item.name = Obtain Item +objective.coreitem.name = Core Item +objective.buildcount.name = Build Count +objective.unitcount.name = Unit Count +objective.destroyunits.name = Destroy Units +objective.timer.name = Timer +objective.destroyblock.name = Destroy Block +objective.destroyblocks.name = Destroy Blocks +objective.destroycore.name = Destroy Core +objective.commandmode.name = Command Mode +objective.flag.name = Flag + +marker.shapetext.name = Shape Text +marker.minimap.name = Minimap +marker.shape.name = Shape +marker.text.name = Text + +marker.background = Background +marker.outline = Outline + objective.research = [accent]Research:\n[]{0}[lightgray]{1} objective.produce = [accent]Obtain:\n[]{0}[lightgray]{1} objective.destroyblock = [accent]Destroy:\n[]{0}[lightgray]{1} @@ -599,6 +621,7 @@ objective.enemiesapproaching = [accent]Enemies approaching in [lightgray]{0}[] objective.destroycore = [accent]Destroy Enemy Core objective.command = [accent]Command Units objective.nuclearlaunch = [accent]\u26A0 Nuclear launch detected: [lightgray]{0} + announce.nuclearstrike = [red]\u26A0 NUCLEAR STRIKE INBOUND \u26A0 loadout = Loadout diff --git a/core/assets/contributors b/core/assets/contributors index 50800b41a6..65c01f3fa9 100644 --- a/core/assets/contributors +++ b/core/assets/contributors @@ -146,3 +146,4 @@ Alex25820 KayAyeAre SMOLKEYS 1stvaliduser(SUS) +GlennFolker diff --git a/core/assets/maps/four.msav b/core/assets/maps/four.msav index d2e133d503..602cb264ad 100644 Binary files a/core/assets/maps/four.msav and b/core/assets/maps/four.msav differ diff --git a/core/assets/maps/lake.msav b/core/assets/maps/lake.msav index f1f705d946..0aa5381fb4 100644 Binary files a/core/assets/maps/lake.msav and b/core/assets/maps/lake.msav differ diff --git a/core/assets/maps/onset.msav b/core/assets/maps/onset.msav index 439424ef24..eb61d46163 100644 Binary files a/core/assets/maps/onset.msav and b/core/assets/maps/onset.msav differ diff --git a/core/assets/maps/split.msav b/core/assets/maps/split.msav index d99ea63ec6..86bc0fd4b1 100644 Binary files a/core/assets/maps/split.msav and b/core/assets/maps/split.msav differ diff --git a/core/assets/maps/two.msav b/core/assets/maps/two.msav index 0c8c9f2fbc..30acd44530 100644 Binary files a/core/assets/maps/two.msav and b/core/assets/maps/two.msav differ diff --git a/core/src/mindustry/content/SectorPresets.java b/core/src/mindustry/content/SectorPresets.java index c5ca6ab12e..9e49b0b9e1 100644 --- a/core/src/mindustry/content/SectorPresets.java +++ b/core/src/mindustry/content/SectorPresets.java @@ -1,8 +1,8 @@ package mindustry.content; import arc.math.geom.*; -import mindustry.game.MapObjectives.*; import mindustry.game.*; +import mindustry.game.MapObjectives.*; import mindustry.graphics.*; import mindustry.type.*; @@ -115,102 +115,18 @@ public class SectorPresets{ addStartingItems = true; alwaysUnlocked = true; difficulty = 1; - - rules = r -> { - r.objectives.addAll( - new ItemObjective(Items.beryllium, 15).withMarkers( - new ShapeTextMarker("Click to mine [accent]resources[] from walls.", 290f * 8f, 106f * 8f) - ), - new BuildCountObjective(Blocks.turbineCondenser, 1).withMarkers( - new ShapeTextMarker("Open the tech tree.\nResearch, then place a [accent]turbine condenser[] on the vent.\nThis will generate [accent]power[].", 289f * 8f, 116f * 8f, 8f * 2.6f, 0f, 9f) - ), - new BuildCountObjective(Blocks.plasmaBore, 1).withMarkers( - new ShapeTextMarker("Research and place a [accent]plasma bore[]. \nThis automatically mines resources from walls.", 293.5f * 8f, 113.5f * 8f, 4f * 2.6f, 45f, 60f) - ), - new BuildCountObjective(Blocks.beamNode, 1).withMarkers( - new ShapeTextMarker("To [accent]power[] the plasma bore, research and place a [accent]beam node[].\nConnect the turbine condenser to the plasma bore.", 294f * 8f, 116f * 8f) - ), - new CoreItemObjective(Items.beryllium, 5).withMarkers( - new TextMarker("Research and place [accent]ducts[] to move the mined resources\nfrom the plasma bore to the core.", 285f * 8f, 108f * 8f) - ), - new CoreItemObjective(Items.beryllium, 200).withMarkers( - new TextMarker("Expand the mining operation.\nPlace more Plasma Bores and use beam nodes and ducts to support them.\nMine 200 beryllium.", 280f * 8f, 118f * 8f) - ), - new CoreItemObjective(Items.graphite, 100).withMarkers( - new TextMarker("More complex blocks require [accent]graphite[].\nSet up plasma bores to mine graphite.", 261f * 8f, 108f * 8f) - ), - new ResearchObjective(Blocks.siliconArcFurnace).withMarkers( - new TextMarker("Begin researching [accent]factories[].\nResearch the [accent]cliff crusher[] and [accent]silicon arc furnace[].", 268f * 8f, 101f * 8f) - ), - new CoreItemObjective(Items.silicon, 50).withMarkers( - new TextMarker("The arc furnace needs [accent]sand[] and [accent]graphite[] to create [accent]silicon[].\n[accent]Power[] is also required.", 268f * 8f, 101f * 8f), - new TextMarker("Use [accent]cliff crushers[] to mine sand.", 262f * 8f, 88f * 8f) - ), - new BuildCountObjective(Blocks.tankFabricator, 1).withMarkers( - new TextMarker("Use [accent]units[] to explore the map, defend buildings, and go on the offensive.\n Research and place a [accent]tank fabricator[].", 258f * 8f, 116f * 8f) - ), - new UnitCountObjective(UnitTypes.stell, 1).withMarkers( - new TextMarker("Produce a unit.\nUse the \"?\" button to see selected factory requirements.", 258f * 8f, 116f * 8f) - ), - new CommandModeObjective().withMarkers( - new TextMarker("Hold [accent]shift[] to enter [accent]command mode[].\n[accent]Left-click and drag[] to select units.\n[accent]Right-click[] to order selected units to move or attack.", 258f * 8f, 116f * 8f) - ), - new BuildCountObjective(Blocks.breach, 1).withMarkers( - new TextMarker("Units are effective, but [accent]turrets[] provide better defensive capabilities if used effectively.\n Place a [accent]Breach[] turret.\nTurrets require [accent]ammo[].", 258f * 8f, 114f * 8f) - ), - new BuildCountObjective(Blocks.berylliumWall, 6).withMarkers( - new TextMarker("[accent]Walls[] can prevent oncoming damage from reaching buildings.\nPlace some [accent]beryllium walls[] around the turret.", 276f * 8f, 133f * 8f) - ), - new TimerObjective("@objective.enemiesapproaching",30 * 60).withMarkers( - new TextMarker("Enemy incoming, prepare to defend.", 276f * 8f, 133f * 8f) - ).withFlags("defStart"), - new DestroyUnitsObjective(2).withFlags("defDone"), - new DestroyBlockObjective(Blocks.coreBastion , 288, 198, Team.malis).withMarkers( - new TextMarker("The enemy is vulnerable. Counter-attack.", 276f * 8f, 133f * 8f) - ), - new BuildCountObjective(Blocks.coreBastion, 1).withMarkers( - new ShapeTextMarker("New cores can be placed on [accent]core tiles[].\nNew cores function as forward bases and share a resource inventory with other cores.\nPlace a core.", 287.5f * 8f, 197.5f * 8f, 9f * 2.6f, 0f, 12f) - ), - new TimerObjective("[accent]Set up defenses:[lightgray] {0}", 120 * 60).withMarkers( - new TextMarker("The enemy will be able to detect you in 2 minutes.\nSet up defenses, mining, and production.", 288f * 8f, 202f * 8f) - ).withFlags("openMap") - ); - }; }}; two = new SectorPreset("two", erekir, 88){{ difficulty = 3; - - rules = r -> { - r.objectives.addAll( - new TimerObjective("[lightgray]Enemy detection:[] [accent]{0}", 7 * 60 * 60).withMarkers( - new TextMarker("The enemy will begin constructing units in 7 minutes.", 276f * 8f, 164f * 8f) - ).withFlags("beginBuilding"), - new ProduceObjective(Items.tungsten).withMarkers( - new ShapeTextMarker("Tungsten can be mined using an [accent]impact drill[].\nThis structure requires [accent]water[] and [accent]power[].", 220f * 8f, 181f * 8f) - ), - new DestroyBlockObjective(Blocks.largeShieldProjector, 210, 278, Team.malis).withMarkers( - new TextMarker("The enemy is protected by shields.\nAn experimental shield breaker module has been detected in this sector.\nFind and activate it using tungsten.", 276f * 8f, 164f * 8f), - new MinimapMarker(23, 137, Pal.accent) - ) - ); - }; }}; lake = new SectorPreset("lake", erekir, 41){{ difficulty = 4; - - rules = r -> { - r.objectives.addAll( - new BuildCountObjective(Blocks.shipFabricator, 1), - new UnitCountObjective(UnitTypes.elude, 1) - ); - }; }}; three = new SectorPreset("three", erekir, 36){{ difficulty = 5; - captureWave = 9; }}; @@ -220,47 +136,10 @@ public class SectorPresets{ split = new SectorPreset("split", erekir, 19){{ //TODO random sector, pick a better one difficulty = 5; - - rules = r -> { - r.objectives.addAll( - new CoreItemObjective(Items.tungsten, 100).withMarkers( - new TextMarker("Some blocks can be picked up by the core unit.\nPick up this [accent]container[] and place it onto the [accent]payload loader[].\n(Default keys are [ and ] to pick up and drop)", 347 * 8f, 445f * 8f), - new TextMarker("You must acquire some tungsten to build units.", 293 * 8f, 417 * 8f) - ), - new BuildCountObjective(Blocks.payloadMassDriver, 2).withMarkers( - new TextMarker("Units must be transported to the other side of the wall.\nPlace two [accent]Payload Mass Drivers[], one on each side of the wall.\nSet up the link by pressing one of them, then selecting the other.", 293 * 8f, 417 * 8f) - ), - new DestroyCoreObjective().withMarkers( - new TextMarker("Similar to the container, units can also be transported using a [accent]Payload Mass Driver[].\nPlace a unit fabricator adjacent to a mass driver to load them, then send them across the wall to attack the enemy base.", 293 * 8f, 417 * 8f) - ) - ); - }; }}; four = new SectorPreset("four", erekir, 29){{ difficulty = 6; - - rules = r -> { - float rad = 52f; - r.objectives.addAll( - new DestroyBlocksObjective(Blocks.coreBastion, Team.malis, Point2.pack(290,501), Point2.pack(158,496)) - .withFlags("nukeannounce"), - new TimerObjective("@objective.nuclearlaunch", 8 * 60 * 60).withMarkers( - new MinimapMarker(338, 377, rad, 14f, Pal.remove), - new ShapeMarker(338 * 8, 377 * 8f){{ - radius = rad * 8f; - fill = true; - color = Pal.remove.cpy().mul(0.8f).a(0.3f); - sides = 90; - }}, - new ShapeMarker(338 * 8, 377 * 8f){{ - radius = rad * 8f; - color = Pal.remove; - sides = 90; - }} - ).withFlags("nuke1") - ); - }; }}; //endregion diff --git a/core/src/mindustry/core/Logic.java b/core/src/mindustry/core/Logic.java index 8eafd646ad..8d99655119 100644 --- a/core/src/mindustry/core/Logic.java +++ b/core/src/mindustry/core/Logic.java @@ -328,52 +328,6 @@ public class Logic implements ApplicationListener{ } } - protected void updateObjectives(){ - //update objectives; do not get completed clientside - if(state.rules.objectives.size > 0){ - var first = state.rules.objectives.first(); - first.update(); - - //initialize markers - for(var marker : first.markers){ - if(!marker.wasAdded){ - marker.wasAdded = true; - marker.added(); - } - } - - boolean completed = false; - - //multiple objectives can be updated in the same frame - while(!net.client() && first != null && first.complete()){ - state.rules.objectives.remove(0); - first.completed(); - //apply flags. - state.rules.objectiveFlags.removeAll(first.flagsRemoved); - state.rules.objectiveFlags.addAll(first.flagsAdded); - if(!headless){ - //delete markers - for(var marker : first.markers){ - if(marker.wasAdded){ - marker.removed(); - marker.wasAdded = false; - } - } - } - - first = state.rules.objectives.firstOpt(); - completed = true; - } - - if(completed){ - //TODO call packet for this? - if(net.server()){ - Call.setRules(state.rules); - } - } - } - } - @Remote(called = Loc.server) public static void sectorCapture(){ //the sector has been conquered - waves get disabled @@ -520,7 +474,10 @@ public class Logic implements ApplicationListener{ //TODO objectives clientside??? if(!state.isEditor()){ - updateObjectives(); + state.rules.objectives.update(); + if(state.rules.objectives.checkChanged() && net.server()){ + Call.setObjectives(state.rules.objectives); + } } if(state.rules.waves && state.rules.waveTimer && !state.gameOver){ diff --git a/core/src/mindustry/core/NetClient.java b/core/src/mindustry/core/NetClient.java index 566b4cdb3d..a792ee3620 100644 --- a/core/src/mindustry/core/NetClient.java +++ b/core/src/mindustry/core/NetClient.java @@ -329,6 +329,11 @@ public class NetClient implements ApplicationListener{ state.rules = rules; } + @Remote(variants = Variant.both) + public static void setObjectives(MapObjectives executor){ + state.rules.objectives = executor; + } + @Remote(variants = Variant.both) public static void worldDataBegin(){ Groups.clear(); diff --git a/core/src/mindustry/editor/MapInfoDialog.java b/core/src/mindustry/editor/MapInfoDialog.java index 83c606db1e..2c7e4e46ab 100644 --- a/core/src/mindustry/editor/MapInfoDialog.java +++ b/core/src/mindustry/editor/MapInfoDialog.java @@ -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); diff --git a/core/src/mindustry/editor/MapObjectivesCanvas.java b/core/src/mindustry/editor/MapObjectivesCanvas.java new file mode 100644 index 0000000000..1126a3dfa4 --- /dev/null +++ b/core/src/mindustry/editor/MapObjectivesCanvas.java @@ -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 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 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)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)); + } + } + } + } +} diff --git a/core/src/mindustry/editor/MapObjectivesDialog.java b/core/src/mindustry/editor/MapObjectivesDialog.java index a1201e3f3f..3a588487b8 100644 --- a/core/src/mindustry/editor/MapObjectivesDialog.java +++ b/core/src/mindustry/editor/MapObjectivesDialog.java @@ -1,14 +1,14 @@ package mindustry.editor; -import arc.*; import arc.func.*; import arc.graphics.*; +import arc.math.geom.*; import arc.scene.event.*; import arc.scene.ui.*; +import arc.scene.ui.TextField.*; import arc.scene.ui.layout.*; import arc.struct.*; import arc.util.*; -import mindustry.*; import mindustry.content.*; import mindustry.ctype.*; import mindustry.game.*; @@ -21,427 +21,658 @@ import mindustry.ui.*; import mindustry.ui.dialogs.*; import mindustry.world.*; +import java.lang.annotation.*; import java.lang.reflect.*; import static mindustry.Vars.*; +import static mindustry.editor.MapObjectivesCanvas.*; +@SuppressWarnings({"unchecked", "rawtypes"}) public class MapObjectivesDialog extends BaseDialog{ - private static final Seq worldFields = Seq.with("x", "y"); + public MapObjectivesCanvas canvas; + protected Cons> out = arr -> {}; - private Seq objectives = new Seq<>(); - private Table list = new Table(); + /** Defines default value providers. */ + private static final ObjectMap, FieldProvider> providers = new ObjectMap<>(); + /** Maps annotation type with its field parsers. Non-annotated fields are mapped with {@link Override}. */ + private static final ObjectMap, ObjectMap, FieldInterpreter>> interpreters = new ObjectMap<>(); - private @Nullable MapObjective selectedObjective; - private @Nullable ObjectiveMarker selectedMarker; - - public MapObjectivesDialog(){ - super("@editor.objectives"); - - buttons.defaults().size(170f, 64f).pad(2f); - buttons.button("@back", Icon.left, this::hide); - - buttons.button("@edit", Icon.edit, () -> { - BaseDialog dialog = new BaseDialog("@editor.export"); - dialog.cont.pane(p -> { - p.margin(10f); - p.table(Tex.button, in -> { - var style = Styles.flatt; - - in.defaults().size(280f, 60f).left(); - - in.button("@waves.copy", Icon.copy, style, () -> { - dialog.hide(); - - Core.app.setClipboardText(JsonIO.write(objectives)); - }).marginLeft(12f).row(); - in.button("@waves.load", Icon.download, style, () -> { - dialog.hide(); - try{ - objectives.set(JsonIO.read(Seq.class, Core.app.getClipboardText())); - - setup(); - }catch(Throwable e){ - ui.showException(e); - } - }).marginLeft(12f).disabled(b -> Core.app.getClipboardText() == null).row(); - in.button("@clear", Icon.none, style, () -> { - dialog.hide(); - objectives.clear(); - setup(); - }).marginLeft(12f).row(); - }); - }); - - dialog.addCloseButton(); - dialog.show(); + static{ + // Default un-annotated field interpreters. + setProvider(String.class, (type, cons) -> cons.get("")); + setInterpreter(String.class, (cont, name, type, field, remover, indexer, get, set) -> { + name(cont, name, remover, indexer); + cont.area(get.get(), set).growX(); }); - buttons.button("@add", Icon.add, () -> { - var selection = new BaseDialog("@add"); - selection.cont.pane(p -> { + setProvider(boolean.class, (type, cons) -> cons.get(false)); + setInterpreter(boolean.class, (cont, name, type, field, remover, indexer, get, set) -> { + name(cont, name, remover, indexer); + cont.check("", get.get(), set::get).growX().fillY().get().getLabelCell().growX(); + }); + + setProvider(byte.class, (type, cons) -> cons.get((byte)0)); + setInterpreter(byte.class, (cont, name, type, field, remover, indexer, get, set) -> { + name(cont, name, remover, indexer); + cont.field(Byte.toString(get.get()), str -> set.get((byte)Strings.parseInt(str))) + .growX().fillY() + .valid(Strings::canParseInt) + .get().setFilter(TextFieldFilter.digitsOnly); + }); + + setProvider(int.class, (type, cons) -> cons.get(0)); + setInterpreter(int.class, (cont, name, type, field, remover, indexer, get, set) -> { + name(cont, name, remover, indexer); + cont.field(Integer.toString(get.get()), str -> set.get(Strings.parseInt(str))) + .growX().fillY() + .valid(Strings::canParseInt) + .get().setFilter(TextFieldFilter.digitsOnly); + }); + + setProvider(float.class, (type, cons) -> cons.get(0f)); + setInterpreter(float.class, (cont, name, type, field, remover, indexer, get, set) -> { + float m = 1f; + if(field != null){ + if(field.isAnnotationPresent(Second.class)){ + m = 60f; + }else if(field.isAnnotationPresent(TilePos.class)){ + m = 8f; + } + } + + float mult = m; + + name(cont, name, remover, indexer); + cont.field(Float.toString(get.get() / mult), str -> set.get(Strings.parseFloat(str) * mult)) + .growX().fillY() + .valid(Strings::canParseFloat) + .get().setFilter(TextFieldFilter.floatsOnly); + }); + + setProvider(UnlockableContent.class, (type, cons) -> cons.get(Blocks.coreShard)); + setInterpreter(UnlockableContent.class, (cont, name, type, field, remover, indexer, get, set) -> { + name(cont, name, remover, indexer); + cont.table(t -> t.left().button( + b -> b.image().size(iconSmall).update(i -> i.setDrawable(get.get().uiIcon)), + () -> showContentSelect(null, set, b -> (field != null && !field.isAnnotationPresent(Researchable.class)) || b.techNode != null) + ).fill().pad(4)).growX().fillY(); + }); + + setProvider(Block.class, (type, cons) -> cons.get(Blocks.copperWall)); + setInterpreter(Block.class, (cont, name, type, field, remover, indexer, get, set) -> { + name(cont, name, remover, indexer); + cont.table(t -> t.left().button( + b -> b.image().size(iconSmall).update(i -> i.setDrawable(get.get().uiIcon)), + () -> showContentSelect(ContentType.block, set, b -> (field != null && !field.isAnnotationPresent(Synthetic.class)) || b.synthetic()) + ).fill().pad(4f)).growX().fillY(); + }); + + setProvider(Item.class, (type, cons) -> cons.get(Items.copper)); + setInterpreter(Item.class, (cont, name, type, field, remover, indexer, get, set) -> { + name(cont, name, remover, indexer); + cont.table(t -> t.left().button( + b -> b.image().size(iconSmall).update(i -> i.setDrawable(get.get().uiIcon)), + () -> showContentSelect(ContentType.item, set, item -> true) + ).fill().pad(4f)).growX().fillY(); + }); + + setProvider(UnitType.class, (type, cons) -> cons.get(UnitTypes.dagger)); + setInterpreter(UnitType.class, (cont, name, type, field, remover, indexer, get, set) -> { + name(cont, name, remover, indexer); + cont.table(t -> t.left().button( + b -> b.image().size(iconSmall).update(i -> i.setDrawable(get.get().uiIcon)), + () -> showContentSelect(ContentType.unit, set, unit -> true) + ).fill().pad(4f)).growX().fillY(); + }); + + setProvider(Team.class, (type, cons) -> cons.get(Team.sharded)); + setInterpreter(Team.class, (cont, name, type, field, remover, indexer, get, set) -> { + name(cont, name, remover, indexer); + cont.table(t -> t.left().button( + b -> b.image(Tex.whiteui).size(iconSmall).update(i -> i.setColor(get.get().color)), + () -> showTeamSelect(set) + ).fill().pad(4f)).growX().fillY(); + }); + + setProvider(Color.class, (type, cons) -> cons.get(Pal.accent.cpy())); + setInterpreter(Color.class, (cont, name, type, field, remover, indexer, get, set) -> { + var out = get.get(); + + name(cont, name, remover, indexer); + cont.table(t -> t.left().button( + b -> b.stack(new Image(Tex.alphaBg), new Image(Tex.whiteui){{ + update(() -> setColor(out)); + }}).grow(), + Styles.squarei, + () -> ui.picker.show(out, res -> set.get(out.set(res))) + ).margin(4f).pad(4f).size(50f)).growX().fillY(); + }); + + setProvider(Vec2.class, (type, cons) -> cons.get(new Vec2())); + setInterpreter(Vec2.class, (cont, name, type, field, remover, indexer, get, set) -> { + var obj = get.get(); + + name(cont, name, remover, indexer); + cont.table(t -> { + boolean isInt = type.raw == int.class; + + FieldInterpreter in = getInterpreter(float.class); + if(isInt) in = getInterpreter(int.class); + + in.build( + t, "x", new TypeInfo(isInt ? int.class : float.class), + field, null, null, + isInt ? () -> (int)obj.x : () -> obj.x, + res -> { + obj.x = isInt ? (Integer)res : (Float)res; + set.get(obj); + } + ); + + in.build( + t.row(), "y", new TypeInfo(isInt ? int.class : float.class), + field, null, null, + isInt ? () -> (int)obj.y : () -> obj.y, + res -> { + obj.y = isInt ? (Integer)res : (Float)res; + set.get(obj); + } + ); + }).growX().fillY(); + }); + + setProvider(Point2.class, (type, cons) -> cons.get(new Point2())); + setInterpreter(Point2.class, (cont, name, type, field, remover, indexer, get, set) -> { + var obj = get.get(); + var vec = new Vec2(obj.x, obj.y); + getInterpreter(Vec2.class).build( + cont, name, new TypeInfo(int.class), + field, remover, indexer, + () -> vec, + res -> { + vec.set(res); + set.get(obj.set((int)vec.x, (int)vec.y)); + } + ); + }); + + // Types that have a provider, but delegate to the default interpreter. + setProvider(MapObjective.class, (type, cons) -> new BaseDialog("@add"){{ + cont.pane(p -> { p.background(Tex.button); - p.marginRight(14); + p.marginRight(14f); p.defaults().size(195f, 56f); + int i = 0; for(var gen : MapObjectives.allObjectiveTypes){ - var objective = gen.get(); - - p.button(objective.typeName(), Styles.flatt, () -> { - objectives.add(objective); - setup(); - selection.hide(); - }).with(Table::left).get().getLabelCell().growX().left().padLeft(5).labelAlign(Align.left); + var obj = gen.get(); + p.button(obj.typeName(), Styles.flatt, () -> { + cons.get(obj); + hide(); + }).with(Table::left).get().getLabelCell().growX().left().padLeft(5f).labelAlign(Align.left); if(++i % 3 == 0) p.row(); } }).scrollX(false); - selection.addCloseButton(); - selection.show(); + addCloseButton(); + show(); + }}); + + setProvider(ObjectiveMarker.class, (type, cons) -> new BaseDialog("@add"){{ + cont.pane(p -> { + p.background(Tex.button); + p.marginRight(14f); + p.defaults().size(195f, 56f); + + int i = 0; + for(var gen : MapObjectives.allMarkerTypes){ + var marker = gen.get(); + p.button(marker.typeName(), Styles.flatt, () -> { + cons.get(marker); + hide(); + }).with(Table::left).get().getLabelCell().growX().left().padLeft(5f).labelAlign(Align.left); + + if(++i % 3 == 0) p.row(); + } + }).scrollX(false); + + addCloseButton(); + show(); + }}); + + // Types that use the default interpreter. It would be nice if all types could use it, but I don't know how to reliably prevent classes like [? extends Content] from using it. + for(var obj : MapObjectives.allObjectiveTypes) setInterpreter(obj.get().getClass(), defaultInterpreter()); + for(var mark : MapObjectives.allMarkerTypes) setInterpreter(mark.get().getClass(), defaultInterpreter()); + + // Annotated field interpreters. + setInterpreter(LabelFlag.class, byte.class, (cont, name, type, field, remover, indexer, get, set) -> { + name(cont, name, remover, indexer); + cont.table(t -> { + t.left().defaults().left(); + byte + value = get.get(), + bg = WorldLabel.flagBackground, out = WorldLabel.flagOutline; + + t.check("@marker.background", (value & bg) == bg, res -> set.get((byte)(res ? value | bg : value & ~bg))) + .growX().fillY() + .padTop(4f).padBottom(4f).get().getLabelCell().growX(); + + t.row(); + t.check("@marker.outline", (value & out) == out, res -> set.get((byte)(res ? value | out : value & ~out))) + .growX().fillY().get().getLabelCell().growX(); + }).growX().fillY(); }); - cont.clear(); - cont.pane(t -> { - list = t; - list.top(); - }).grow(); - } - - public void show(Seq objectives){ - super.show(); - selectedObjective = null; - - this.objectives = objectives; - setup(); - } - - void setup(){ - list.clear(); - - for(var objective : objectives){ - list.table(Tex.button, t -> { - t.margin(0); - - t.button(b -> { - b.left(); - b.add(objective.typeName()).color(Pal.accent); - - b.add().growX(); - - b.button(Icon.upOpen, Styles.emptyi, () -> { - int index = objectives.indexOf(objective); - if(index > 0){ - objectives.swap(index, index - 1); - setup(); - } - }).pad(-6).size(46f); - - b.button(Icon.downOpen, Styles.emptyi, () -> { - int index = objectives.indexOf(objective); - if(index < objectives.size - 1){ - objectives.swap(index, index + 1); - setup(); - } - }).pad(-6).size(46f); - - b.button(Icon.cancel, Styles.emptyi, () -> { - objectives.remove(objective); - list.getCell(t).pad(0f); - - t.remove(); - setup(); - }).pad(-6).size(46f).padRight(-12f); - }, () -> { - if(selectedObjective != objective){ - selectedObjective = objective; - setup(); - } - }).growX().height(46f).pad(-6f).padBottom(0f).row(); - - if(selectedObjective == objective){ - t.table(f -> { - f.left(); - f.margin(10f); - - f.defaults().minHeight(40f).left(); - - var fields = objective.getClass().getFields(); - - for(var field : fields){ - if((field.getModifiers() & Modifier.PUBLIC) == 0) continue; - - displayField(f, field, objective); - } - - }).grow(); - } - - }).width(340f).pad(8f).row(); - } - } - - void displayField(Table f, Field field, Object objective){ - f.add(field.getName() + ": "); - - var type = field.getType(); - - if(type == String.class){ - f.area(Reflect.get(objective, field), text -> { - Reflect.set(objective, field, text); - }).height(60f); - }else if(type == boolean.class){ - f.check("", Reflect.get(objective, field), val -> Reflect.set(objective, field, val)); - }else if(type == int.class){ - f.field(Reflect.get(objective, field) + "", text -> { - if(Strings.canParseInt(text)){ - Reflect.set(objective, field, Strings.parseInt(text)); - } - }).valid(Strings::canParseInt); - }else if(type == float.class){ - float multiplier = worldFields.contains(field.getName()) ? tilesize : 1f; - - f.field((Reflect.get(objective, field) / multiplier) + "", text -> { - if(Strings.canParsePositiveFloat(text)){ - Reflect.set(objective, field, Strings.parseFloat(text) * multiplier); - } - }).valid(Strings::canParseFloat); - }else if(type == UnlockableContent.class){ - - f.button(b -> b.image(Reflect.get(objective, field).uiIcon).size(iconSmall), () -> { - showContentSelect(null, result -> { - Reflect.set(objective, field, result); - setup(); - }, b -> b.techNode != null); - }).pad(4); - - }else if(type == Block.class){ - f.button(b -> b.image(Reflect.get(objective, field).uiIcon).size(iconSmall), () -> { - showContentSelect(ContentType.block, result -> { - Reflect.set(objective, field, result); - setup(); - }, b -> ((Block)b).synthetic()); - }).pad(4); - }else if(type == Item.class){ - f.button(b -> b.image(Reflect.get(objective, field).uiIcon).size(iconSmall), () -> { - showContentSelect(ContentType.item, result -> { - Reflect.set(objective, field, result); - setup(); - }, b -> true); - }).pad(4); - }else if(type == Team.class){ - f.button(b -> b.image(Tex.whiteui).color(Reflect.get(objective, field).color).size(iconSmall), () -> { - showTeamSelect(result -> { - Reflect.set(objective, field, result); - setup(); - }); - }).pad(4); - }else if(type == Color.class){ - Color fieldCol = Reflect.get(objective, field); - - f.table(Tex.pane, in -> { - in.stack(new Image(Tex.alphaBg), new Image(Tex.whiteui){{ - update(() -> setColor(fieldCol)); - }}).grow(); - }).margin(4).size(50f).get().clicked(() -> ui.picker.show(fieldCol, fieldCol::set)); - - }else if(type == String[].class){ - - Table strings = new Table(); - strings.marginLeft(20f); + // Special data structure interpreters. + // Instantiate default `Seq`s with a reflectively allocated array. + setProvider(Seq.class, (type, cons) -> cons.get(new Seq<>(type.element.raw))); + setInterpreter(Seq.class, (cont, name, type, field, remover, indexer, get, set) -> cont.table(main -> { Runnable[] rebuild = {null}; + var arr = get.get(); - strings.left(); + main.margin(0f, 10f, 0f, 10f); + var header = main.table(Tex.button, t -> { + t.left(); + t.margin(10f); - float h = 40f; + if(name.length() > 0) t.add(name + ":").color(Pal.accent); + t.add().growX(); - rebuild[0] = () -> { - strings.clear(); - strings.left().defaults().padBottom(3f).padTop(3f); - String[] array = Reflect.get(objective, field); - - for(int i = 0; i < array.length; i++){ - int fi = i; - var str = array[i]; - strings.field(str, result -> { - array[fi] = result; - }).maxTextLength(20).height(h); - - strings.button(Icon.cancel, Styles.squarei, () -> { - Reflect.set(objective, field, Structs.remove(array, fi)); - - rebuild[0].run(); - }).padLeft(4).size(h); - - strings.row(); + if(remover != null) t.button(Icon.trash, Styles.emptyi, remover).fill().padRight(4f); + if(indexer != null){ + t.button(Icon.upOpen, Styles.emptyi, () -> indexer.get(true)).fill().padRight(4f); + t.button(Icon.downOpen, Styles.emptyi, () -> indexer.get(false)).fill().padRight(4f); } - strings.button("+ Add", () -> { - Reflect.set(objective, field, Structs.add(array, "")); - + t.button(Icon.add, Styles.emptyi, () -> getProvider(type.element.raw).get(type.element, res -> { + arr.add(res); rebuild[0].run(); - }).height(h).width(140f).padLeft(-20f).left().row(); - }; + })).fill(); + }).growX().height(46f).pad(0f, -10f, 0f, -10f).get(); + main.row().table(Tex.button, t -> rebuild[0] = () -> { + t.clear(); + t.top(); + + if(arr.isEmpty()){ + t.background(Tex.clear).margin(0f).setSize(0f); + }else{ + t.background(Tex.button).margin(10f).marginTop(20f); + } + + for(int i = 0, len = arr.size; i < len; i++){ + int index = i; + if(index > 0) t.row(); + + getInterpreter((Class)arr.get(index).getClass()).build( + t, "", new TypeInfo(arr.get(index).getClass()), + field, () -> { + arr.remove(index); + rebuild[0].run(); + }, field == null || !field.isAnnotationPresent(Unordered.class) ? in -> { + if(in && index > 0){ + arr.swap(index, index - 1); + rebuild[0].run(); + }else if(!in && index < len - 1){ + arr.swap(index, index + 1); + rebuild[0].run(); + } + } : null, + () -> arr.get(index), + res -> { + arr.set(index, res); + set.get(arr); + } + ); + } + + set.get(arr); + }).padTop(-10f).growX().fillY(); rebuild[0].run(); - f.row(); - f.add(strings).colspan(2).fill(); - }else if(type == ObjectiveMarker[].class){ - Runnable[] rebuild = {null}; + header.toFront(); + }).growX().fillY().pad(4f).colspan(2)); - f.row(); - - f.table(t -> { - t.margin(0).marginLeft(10f); - - rebuild[0] = () -> { - t.clear(); - - t.left().defaults().growX().left(); - - ObjectiveMarker[] array = Reflect.get(objective, field); - - for(var marker : array){ - t.button(b -> { - b.left(); - b.add(marker.typeName()).color(Pal.accent); - - b.add().growX(); - - b.button(Icon.upOpen, Styles.emptyi, () -> { - int index = Structs.indexOf(array, marker); - if(index > 0){ - Structs.swap(array, index, index - 1); - rebuild[0].run(); - } - }).pad(-6).size(46f); - - b.button(Icon.downOpen, Styles.emptyi, () -> { - int index = Structs.indexOf(array, marker); - if(index < objectives.size - 1){ - Structs.swap(array, index, index + 1); - rebuild[0].run(); - } - }).pad(-6).size(46f); - - b.button(Icon.cancel, Styles.emptyi, () -> { - Reflect.set(objective, field, Structs.remove(array, marker)); - - t.getCell(b).pad(0f); - b.remove(); - rebuild[0].run(); - }).pad(-6).size(46f).padRight(-12f); - }, () -> { - if(selectedMarker != marker){ - selectedMarker = marker; - rebuild[0].run(); - } - }).width(280f).growX().height(46f).pad(-6f).padBottom(12f).row(); - - if(selectedMarker == marker){ - t.table(b -> { - b.left(); - b.margin(10f); - - b.defaults().minHeight(40f).left(); - - var fields = marker.getClass().getFields(); - - for(var disp : fields){ - if((disp.getModifiers() & Modifier.TRANSIENT) != 0) continue; - - displayField(b, disp, marker); - } - - }).padTop(-12f).grow().row(); - } - } - - t.button("+ Add", () -> { - var selection = new BaseDialog("@add"); - selection.cont.pane(p -> { - p.background(Tex.button); - p.marginRight(14); - p.defaults().size(195f, 56f); - int i = 0; - for(var gen : MapObjectives.allMarkerTypes){ - var marker = gen.get(); - - p.button(marker.typeName(), Styles.flatt, () -> { - Reflect.set(objective, field, Structs.add(Reflect.get(objective, field), marker)); - rebuild[0].run(); - selection.hide(); - }).with(Table::left).get().getLabelCell().growX().left().padLeft(5).labelAlign(Align.left); - - if(++i % 3 == 0) p.row(); - } - }).scrollX(false); - - selection.addCloseButton(); - selection.show(); - }).height(40f).width(140f).left().padLeft(-18f).padTop(-6f).row(); - }; - - rebuild[0].run(); - - }).width(280f).pad(8f).colspan(2).row(); - - }else if(type == byte.class){ - f.table(t -> { - byte value = Reflect.get(objective, field); - t.left().defaults().left(); - t.check("background", (value & WorldLabel.flagBackground) != 0, val -> - Reflect.set(objective, field, (byte)(val ? value | WorldLabel.flagBackground : value & ~WorldLabel.flagBackground))).padTop(4f).padBottom(4f); - t.row(); - t.check("outline", (value & WorldLabel.flagOutline) != 0, val -> - Reflect.set(objective, field, (byte)(val ? value | WorldLabel.flagOutline : value & ~WorldLabel.flagOutline))); - }); - }else{ - f.add("[red]UNFINISHED"); - } - - f.row(); + // Reserved for array types that are not explicitly handled. Essentially handles it the same way as `Seq`. + setProvider(Object[].class, (type, cons) -> cons.get(Reflect.newArray(type.element.raw, 0))); + setInterpreter(Object[].class, (cont, name, type, field, remover, indexer, get, set) -> { + var arr = Seq.with(get.get()); + getInterpreter(Seq.class).build( + cont, name, new TypeInfo(Seq.class, type.element), + field, remover, indexer, + () -> arr, + res -> set.get(arr.toArray(type.element.raw)) + ); + }); } - void showContentSelect(@Nullable ContentType type, Cons cons, Boolf check){ - BaseDialog dialog = new BaseDialog(""); - dialog.cont.pane(p -> { - int i = 0; - for(var block : (type == null ? Vars.content.blocks().copy().as() - .add(Vars.content.items()) - .add(Vars.content.liquids()) - .add(Vars.content.units()) : - Vars.content.getBy(type).as())){ + public static FieldInterpreter defaultInterpreter(){ + return (cont, name, type, field, remover, indexer, get, set) -> cont.table(main -> { + main.margin(0f, 10f, 0f, 10f); + var header = main.table(Tex.button, t -> { + t.left(); + t.margin(10f); - if(!check.get(block)) continue; + if(name.length() > 0) t.add(name + ":").color(Pal.accent); + t.add().growX(); - p.image(block == Blocks.air ? Icon.none.getRegion() : block.uiIcon).size(iconMed).pad(3) - .with(b -> b.addListener(new HandCursorListener())) - .tooltip(block.localizedName).get().clicked(() -> { - cons.get(block); - dialog.hide(); - }); - if(++i % 10 == 0) p.row(); - } + Cell remove = null; + if(remover != null) remove = t.button(Icon.trash, Styles.emptyi, remover).fill(); + if(indexer != null){ + if(remove != null) remove.padRight(4f); + t.button(Icon.upOpen, Styles.emptyi, () -> indexer.get(true)).fill().padRight(4f); + t.button(Icon.downOpen, Styles.emptyi, () -> indexer.get(false)).fill(); + } + }).growX().height(46f).pad(0f, -10f, -0f, -10f).get(); + + main.row().table(Tex.button, t -> { + t.left(); + t.top().margin(10f).marginTop(20f); + + t.defaults().minHeight(40f).left(); + var obj = get.get(); + + int i = 0; + for(var e : JsonIO.json.getFields(type.raw).values()){ + if(i++ > 0) t.row(); + + var f = e.field; + var ft = f.getType(); + int mods = f.getModifiers(); + + if(!Modifier.isPublic(mods) || (Modifier.isFinal(mods) && ( + String.class.isAssignableFrom(ft) || + unbox(ft).isPrimitive() + ))) continue; + + var anno = Structs.find(f.getDeclaredAnnotations(), a -> hasInterpreter(a.annotationType(), ft)); + getInterpreter(anno == null ? Override.class : anno.annotationType(), ft).build( + t, f.getName(), new TypeInfo(f), + f, null, null, + () -> Reflect.get(obj, f), + Modifier.isFinal(mods) ? res -> {} : res -> Reflect.set(obj, f, res) + ); + } + }).padTop(-10f).growX().fillY(); + + header.toFront(); + }).growX().fillY().pad(4f).colspan(2); + } + + public static void name(Table cont, CharSequence name, @Nullable Runnable remover, @Nullable Boolc indexer){ + if(indexer != null || remover != null){ + cont.table(t -> { + if(remover != null) t.button(Icon.trash, Styles.emptyi, remover).fill().padRight(4f); + if(indexer != null){ + t.button(Icon.upOpen, Styles.emptyi, () -> indexer.get(true)).fill().padRight(4f); + t.button(Icon.downOpen, Styles.emptyi, () -> indexer.get(false)).fill().padRight(4f); + } + }).fill(); + }else{ + cont.add(name + ": "); + } + } + + public MapObjectivesDialog(){ + super("@editor.objectives"); + clear(); + margin(0f); + + stack( + canvas = new MapObjectivesCanvas(), + new Table(){{ + buttons.defaults().size(170f, 64f).pad(2f); + buttons.button("@back", Icon.left, MapObjectivesDialog.this::hide); + buttons.button("@add", Icon.add, () -> getProvider(MapObjective.class).get(new TypeInfo(MapObjective.class), canvas::query)); + + if(mobile){ + buttons.row(); + buttons.button("@cancel", Icon.cancel, canvas::stopQuery).disabled(b -> !canvas.isQuerying()); + buttons.button("@ok", Icon.ok, canvas::placeQuery).disabled(b -> !canvas.isQuerying()); + } + + setFillParent(true); + margin(3f); + + add(titleTable).growX().fillY(); + row().add().grow(); + row().add(buttons).fill(); + addCloseListener(); + }} + ).grow().pad(0f).margin(0f); + + hidden(() -> { + out.get(canvas.objectives); + out = arr -> {}; }); + } + + public void show(Seq objectives, Cons> out){ + this.out = out; + + canvas.clearObjectives(); + if( + objectives.any() && ( + // If the objectives were previously programmatically made... + objectives.contains(obj -> obj.editorX == -1 || obj.editorY == -1) || + // ... or some idiot somehow made it not work... + objectives.contains(obj -> !canvas.tilemap.createTile(obj)) + )){ + // ... then rebuild the structure. + canvas.clearObjectives(); + + // This is definitely NOT a good way to do it, but only insane people or people from the distant past would actually encounter this anyway. + int w = objWidth + 2, + len = objectives.size * w, + columns = objectives.size, + rows = 1; + + if(len > bounds){ + rows = len / bounds; + columns = bounds / w; + } + + int i = 0; + loop: + for(int y = 0; y < rows; y++){ + for(int x = 0; x < columns; x++){ + canvas.tilemap.createTile(x * w, bounds - 1 - y * 2, objectives.get(i++)); + if(i >= objectives.size) break loop; + } + } + } + + canvas.objectives.set(objectives); + show(); + } + + public static void showContentSelect(@Nullable ContentType type, Cons cons, Boolf check){ + BaseDialog dialog = new BaseDialog(""); + dialog.cont.pane(Styles.noBarPane, t -> { + int i = 0; + for(var content : (type == null ? content.blocks().copy().as() + .add(content.items()) + .add(content.liquids()) + .add(content.units()) : + content.getBy(type).as() + )){ + if(content.isHidden() || !check.get((T)content)) continue; + t.image(content == Blocks.air ? Icon.none.getRegion() : content.uiIcon).size(iconMed).pad(3) + .with(b -> b.addListener(new HandCursorListener())) + .tooltip(content.localizedName).get().clicked(() -> { + cons.get((T)content); + dialog.hide(); + }); + + if(++i % 10 == 0) t.row(); + } + }).fill(); dialog.closeOnBack(); dialog.show(); } - void showTeamSelect(Cons cons){ + public static void showTeamSelect(Cons cons){ BaseDialog dialog = new BaseDialog(""); for(var team : Team.baseTeams){ - dialog.cont.image(Tex.whiteui).size(iconMed).color(team.color).pad(4) - .with(i -> i.addListener(new HandCursorListener())) - .tooltip(team.localized()).get().clicked(() -> { - cons.get(team); - dialog.hide(); - }); + .with(i -> i.addListener(new HandCursorListener())) + .tooltip(team.localized()).get().clicked(() -> { + cons.get(team); + dialog.hide(); + }); } dialog.closeOnBack(); dialog.show(); } + + public static Class unbox(Class boxed){ + return switch(boxed.getSimpleName()){ + case "Boolean" -> boolean.class; + case "Byte" -> byte.class; + case "Character" -> char.class; + case "Short" -> short.class; + case "Integer" -> int.class; + case "Long" -> long.class; + case "Float" -> float.class; + case "Double" -> double.class; + default -> boxed; + }; + } + + public static void setInterpreter(Class type, FieldInterpreter interpreter){ + setInterpreter(Override.class, type, interpreter); + } + + public static void setInterpreter(Class anno, Class type, FieldInterpreter interpreter){ + interpreters.get(anno, ObjectMap::new).put(type, interpreter); + } + + public static boolean hasInterpreter(Class type){ + return hasInterpreter(Override.class, type); + } + + public static boolean hasInterpreter(Class anno, Class type){ + return interpreters.get(anno, ObjectMap::new).containsKey(unbox(type)); + } + + public static FieldInterpreter getInterpreter(Class type){ + return getInterpreter(Override.class, type); + } + + public static FieldInterpreter getInterpreter(Class anno, Class type){ + if(hasInterpreter(anno, type)){ + return (FieldInterpreter)interpreters.get(anno, ObjectMap::new).get(unbox(type)); + }else if(hasInterpreter(Override.class, type)){ + return (FieldInterpreter)interpreters.get(Override.class, ObjectMap::new).get(unbox(type)); + }else if(type.isArray() && !type.getComponentType().isPrimitive()){ + return (FieldInterpreter)(hasInterpreter(anno, Object[].class) + ? interpreters.get(anno).get(Object[].class) + : interpreters.get(Override.class).get(Object[].class) + ); + }else{ + throw new IllegalArgumentException("Interpreter for type " + type + " not set up yet."); + } + } + + public static void setProvider(Class type, FieldProvider provider){ + providers.put(unbox(type), provider); + } + + public static boolean hasProvider(Class type){ + return providers.containsKey(unbox(type)); + } + + public static FieldProvider getProvider(Class type){ + return (FieldProvider)providers.getThrow(unbox(type), () -> new IllegalArgumentException("Provider for type " + type + " not set up yet.")); + } + + public interface FieldInterpreter{ + /** + * Builds the interpreter for (not-necessarily) a possibly annotated field. Implementations must add exactly + * 2 columns to the table. + * @param name May be empty. + * @param remover If this callback is not {@code null}, this interpreter should add a button that invokes the + * callback to signal element removal. + * @param indexer If this callback is not {@code null}, this interpreter should add 2 buttons that invoke the + * callback to signal element rearrangement with the following values:
    + *
  • {@code true}: Swap element with previous index.
  • + *
  • {@code false}: Swap element with next index.
  • + *
+ */ + void build(Table cont, + CharSequence name, TypeInfo type, + @Nullable Field field, + @Nullable Runnable remover, @Nullable Boolc indexer, + Prov get, Cons set); + } + + public interface FieldProvider{ + void get(TypeInfo type, Cons cons); + } + + /** + * Stores parameterized or array type information for convenience. + * For {@code A[]}: {@link #raw} is {@code A[]}, {@link #element} is {@code A}, {@link #key} is {@code null}. + * For {@code Seq}: {@link #raw} is {@link Seq}, {@link #element} is {@code A}, {@link #key} is {@code null}. + * For {@code ObjectMap}: {@link #raw} is {@link ObjectMap}, {@link #element} is {@code B}, {@link #key} is {@code A}. + */ + public static class TypeInfo{ + public final Class raw; + public final TypeInfo element, key; + + public TypeInfo(Field field){ + this(field.getType(), field.getGenericType()); + } + + public TypeInfo(Class raw){ + this(raw, raw); + } + + /** Use with care! */ + public TypeInfo(Class raw, TypeInfo element){ + this.raw = unbox(raw); + this.element = element; + key = null; + } + + public TypeInfo(Class raw, Type generic){ + this.raw = unbox(raw); + if(raw.isArray()){ + key = null; + element = new TypeInfo(raw.getComponentType(), generic instanceof GenericArrayType type ? type.getGenericComponentType() : raw.getComponentType()); + }else if(Seq.class.isAssignableFrom(raw)){ + key = null; + element = getParam(generic, 0); + }else if(ObjectMap.class.isAssignableFrom(raw)){ + key = getParam(generic, 0); + element = getParam(generic, 1); + }else{ + key = element = null; + } + } + + public static TypeInfo getParam(Type generic, int index){ + Type[] params = + generic instanceof ParameterizedType type ? type.getActualTypeArguments() : + generic instanceof GenericDeclaration type ? type.getTypeParameters() : null; + + if(params != null && index < params.length){ + var target = params[index]; + return new TypeInfo(raw(target), target); + } + + return new TypeInfo(Object.class, Object.class); + } + + public static Class raw(Type type){ + if(type instanceof Class c) return c; + if(type instanceof ParameterizedType c) return (Class)c.getRawType(); + if(type instanceof GenericArrayType c) return Reflect.newArray(raw(c.getGenericComponentType()), 0).getClass(); + if(type instanceof TypeVariable c) return raw(c.getBounds()[0]); + return Object.class; + } + } } diff --git a/core/src/mindustry/game/MapObjectives.java b/core/src/mindustry/game/MapObjectives.java index a6a4736629..3a15d27b8d 100644 --- a/core/src/mindustry/game/MapObjectives.java +++ b/core/src/mindustry/game/MapObjectives.java @@ -7,70 +7,311 @@ import arc.graphics.g2d.*; import arc.math.*; import arc.math.geom.*; import arc.scene.ui.layout.*; +import arc.struct.*; import arc.util.*; +import mindustry.*; import mindustry.content.*; import mindustry.ctype.*; +import mindustry.game.MapObjectives.*; import mindustry.gen.*; import mindustry.graphics.*; +import mindustry.io.*; import mindustry.type.*; import mindustry.world.*; +import java.lang.annotation.*; +import java.util.*; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.*; import static mindustry.Vars.*; -public class MapObjectives{ - public static Prov[] allObjectiveTypes = new Prov[]{ - ResearchObjective::new, BuildCountObjective::new, UnitCountObjective::new, ItemObjective::new, - CommandModeObjective::new, CoreItemObjective::new, DestroyCoreObjective::new, DestroyUnitsObjective::new, - TimerObjective::new, FlagObjective::new, DestroyBlockObjective::new, ProduceObjective::new, - DestroyBlocksObjective::new - }; +/** Handles and executes in-map objectives. */ +public class MapObjectives implements Iterable, Eachable{ + public static final Seq> allObjectiveTypes = new Seq<>(); + public static final Seq> allMarkerTypes = new Seq<>(); - public static Prov[] allMarkerTypes = new Prov[]{ - TextMarker::new, ShapeMarker::new, ShapeTextMarker::new, MinimapMarker::new - }; + /** + * All objectives the executor contains. Do not modify directly, ever! + * @see #eachRunning(Cons) + */ + public Seq all = new Seq<>(4); + /** @see #checkChanged() */ + protected transient boolean changed; + + static{ + registerObjective( + ResearchObjective::new, + ProduceObjective::new, + ItemObjective::new, + CoreItemObjective::new, + BuildCountObjective::new, + UnitCountObjective::new, + DestroyUnitsObjective::new, + TimerObjective::new, + DestroyBlockObjective::new, + DestroyBlocksObjective::new, + DestroyCoreObjective::new, + CommandModeObjective::new, + FlagObjective::new + ); + + registerMarker( + ShapeTextMarker::new, + MinimapMarker::new, + ShapeMarker::new, + TextMarker::new + ); + } + + @SafeVarargs + public static void registerObjective(Prov... providers){ + for(var prov : providers){ + allObjectiveTypes.add(prov); + + Class type = prov.get().getClass(); + JsonIO.classTag(type.getSimpleName().replace("Objective", ""), type); + } + } + + @SafeVarargs + public static void registerMarker(Prov... providers){ + for(var prov : providers){ + allMarkerTypes.add(prov); + + Class type = prov.get().getClass(); + JsonIO.classTag(type.getSimpleName().replace("Marker", ""), type); + } + } + + /** Adds all given objectives to the executor as root objectives. */ + public void add(MapObjective... objectives){ + for(var objective : objectives) flatten(objective); + } + + /** Recursively adds the objective and its children. */ + private void flatten(MapObjective objective){ + for(var child : objective.children) flatten(child); + + objective.children.clear(); + all.add(objective); + } + + /** Updates all objectives this executor contains. */ + public void update(){ + //TODO am i doing this correctly + if(net.client()) return; + eachRunning(obj -> { + for(var marker : obj.markers){ + if(!marker.wasAdded){ + marker.wasAdded = true; + marker.added(); + } + } + + if(obj.update()){ + obj.completed = true; + obj.done(); + for(var marker : obj.markers){ + if(marker.wasAdded){ + marker.removed(); + marker.wasAdded = false; + } + } + } + + changed |= obj.changed; + obj.changed = false; + }); + } + + /** @return True if map rules should be synced. Reserved for {@link Vars#logic}; do not invoke directly! */ + public boolean checkChanged(){ + boolean has = changed; + changed = false; + + return has; + } + + /** @return Whether there are any qualified objectives at all. */ + public boolean any(){ + return all.count(MapObjective::qualified) > 0; + } + + /** Iterates over all qualified in-map objectives. */ + public void eachRunning(Cons cons){ + all.each(MapObjective::qualified, cons); + } + + /** Iterates over all qualified in-map objectives, with a filter. */ + public void eachRunning(Boolf pred, Cons cons){ + all.each(obj -> obj.qualified() && pred.get(obj), cons); + } + + @Override + public Iterator iterator(){ + return all.iterator(); + } + + @Override + public void each(Cons cons){ + all.each(cons); + } + + /** Base abstract class for any in-map objective. */ + public static abstract class MapObjective{ + public @Nullable String details; + public @Unordered String[] flagsAdded = {}; + public @Unordered String[] flagsRemoved = {}; + public ObjectiveMarker[] markers = {}; + + /** The parents of this objective. All parents must be done in order for this to be updated. */ + public transient Seq parents = new Seq<>(2); + /** Temporary container to store references since this class is static. Will immediately be flattened. */ + private transient final Seq children = new Seq<>(2); + + /** For the objectives UI dialog. Do not modify directly! */ + public transient int editorX = -1, editorY = -1; + + /** Whether this objective has been done yet. This is internally set. */ + private boolean completed; + /** Internal value. Do not modify! */ + private transient boolean depFinished, changed; + + /** @return True if this objective is done and should be removed from the executor. */ + public abstract boolean update(); + + /** Reset internal state, if any. */ + public void reset(){} + + /** Called once after {@link #update()} returns true, before this objective is removed. */ + public void done(){ + changed(); + state.rules.objectiveFlags.removeAll(flagsRemoved); + state.rules.objectiveFlags.addAll(flagsAdded); + } + + /** Notifies the executor that map rules should be synced. */ + protected void changed(){ + changed = true; + } + + /** @return True if all {@link #parents} are completed, rendering this objective able to execute. */ + public final boolean dependencyFinished(){ + if(depFinished) return true; + + boolean f = true; + for(var parent : parents){ + if(!parent.isCompleted()) return false; + } + + return f && (depFinished = true); + } + + /** @return True if this objective is done (practically, has been removed from the executor). */ + public final boolean isCompleted(){ + return completed; + } + + /** @return Whether this objective should run at all. */ + public boolean qualified(){ + return !completed && dependencyFinished(); + } + + /** @return This objective, with the given child's parents added with this, for chaining operations. */ + public MapObjective child(MapObjective child){ + child.parents.add(this); + children.add(child); + return this; + } + + /** @return This objective, with the given parent added to this objective's parents, for chaining operations. */ + public MapObjective parent(MapObjective parent){ + parents.add(parent); + return this; + } + + /** @return This objective, with the details message assigned to, for chaining operations. */ + public MapObjective details(String details){ + this.details = details; + return this; + } + + /** @return This objective, with the added-flags assigned to, for chaining operations. */ + public MapObjective flagsAdded(String... flagsAdded){ + this.flagsAdded = flagsAdded; + return this; + } + + /** @return This objective, with the removed-flags assigned to, for chaining operations. */ + public MapObjective flagsRemoved(String... flagsRemoved){ + this.flagsRemoved = flagsRemoved; + return this; + } + + /** @return This objective, with the markers assigned to, for chaining operations. */ + public MapObjective markers(ObjectiveMarker... markers){ + this.markers = markers; + return this; + } + + /** @return Basic mission display text. If null, falls back to standard text. */ + public @Nullable String text(){ + return null; + } + + /** @return Details that appear upon click. */ + public @Nullable String details(){ + return details; + } + + /** @return The localized type-name of this objective, defaulting to the class simple name without the "Objective" prefix. */ + public String typeName(){ + String className = getClass().getSimpleName().replace("Objective", ""); + return Core.bundle == null ? className : Core.bundle.get("objective." + className.toLowerCase() + ".name", className); + } + } /** Research a specific piece of content in the tech tree. */ public static class ResearchObjective extends MapObjective{ - public UnlockableContent content = Items.copper; + public @Researchable UnlockableContent content = Items.copper; public ResearchObjective(UnlockableContent content){ this.content = content; } - public ResearchObjective(){ + public ResearchObjective(){} + + @Override + public boolean update(){ + return content.unlocked(); } @Override public String text(){ return Core.bundle.format("objective.research", content.emoji(), content.localizedName); } - - @Override - public boolean complete(){ - return content.unlocked(); - } } /** Produce a specific piece of content in the tech tree (essentially research with different text). */ public static class ProduceObjective extends MapObjective{ - public UnlockableContent content = Items.copper; + public @Researchable UnlockableContent content = Items.copper; public ProduceObjective(UnlockableContent content){ this.content = content; } - public ProduceObjective(){ + public ProduceObjective(){} + + @Override + public boolean update(){ + return content.unlocked(); } @Override public String text(){ return Core.bundle.format("objective.produce", content.emoji(), content.localizedName); } - - @Override - public boolean complete(){ - return content.unlocked(); - } } /** Have a certain amount of item in your core. */ @@ -83,18 +324,17 @@ public class MapObjectives{ this.amount = amount; } - public ItemObjective(){ + public ItemObjective(){} + + @Override + public boolean update(){ + return state.rules.defaultTeam.items().has(item, amount); } @Override public String text(){ return Core.bundle.format("objective.item", state.rules.defaultTeam.items().get(item), amount, item.emoji(), item.localizedName); } - - @Override - public boolean complete(){ - return state.rules.defaultTeam.items().has(item, amount); - } } /** Get a certain item in your core (through a block, not manually.) */ @@ -107,23 +347,22 @@ public class MapObjectives{ this.amount = amount; } - public CoreItemObjective(){ + public CoreItemObjective(){} + + @Override + public boolean update(){ + return state.stats.coreItemCount.get(item) >= amount; } @Override public String text(){ return Core.bundle.format("objective.coreitem", state.stats.coreItemCount.get(item), amount, item.emoji(), item.localizedName); } - - @Override - public boolean complete(){ - return state.stats.coreItemCount.get(item) >= amount; - } } /** Build a certain amount of a block. */ public static class BuildCountObjective extends MapObjective{ - public Block block = Blocks.conveyor; + public @Synthetic Block block = Blocks.conveyor; public int count = 1; public BuildCountObjective(Block block, int count){ @@ -131,18 +370,17 @@ public class MapObjectives{ this.count = count; } - public BuildCountObjective(){ + public BuildCountObjective(){} + + @Override + public boolean update(){ + return state.stats.placedBlockCount.get(block, 0) >= count; } @Override public String text(){ return Core.bundle.format("objective.build", count, block.emoji(), block.localizedName); } - - @Override - public boolean complete(){ - return state.stats.placedBlockCount.get(block, 0) >= count; - } } /** Produce a certain amount of a unit. */ @@ -155,18 +393,17 @@ public class MapObjectives{ this.count = count; } - public UnitCountObjective(){ + public UnitCountObjective(){} + + @Override + public boolean update(){ + return state.rules.defaultTeam.data().countType(unit) >= count; } @Override public String text(){ return Core.bundle.format("objective.buildunit", count, unit.emoji(), unit.localizedName); } - - @Override - public boolean complete(){ - return state.rules.defaultTeam.data().countType(unit) >= count; - } } /** Produce a certain amount of units. */ @@ -177,24 +414,24 @@ public class MapObjectives{ this.count = count; } - public DestroyUnitsObjective(){ + public DestroyUnitsObjective(){} + + @Override + public boolean update(){ + return state.stats.enemyUnitsDestroyed >= count; } @Override public String text(){ return Core.bundle.format("objective.destroyunits", count); } - - @Override - public boolean complete(){ - return state.stats.enemyUnitsDestroyed >= count; - } } public static class TimerObjective extends MapObjective{ public String text; - public float countup; - public float duration = 60f * 30f; + public @Second float duration = 60f * 30f; + + protected float countup; public TimerObjective(String text, float duration){ this.text = text; @@ -205,13 +442,8 @@ public class MapObjectives{ } @Override - public boolean complete(){ - return countup >= duration; - } - - @Override - public void update(){ - countup += Time.delta; + public boolean update(){ + return (countup += Time.delta) >= duration; } @Override @@ -243,28 +475,27 @@ public class MapObjectives{ return Core.bundle.formatString(text, timeString.toString()); } } - return text; + + return null; } } public static class DestroyBlockObjective extends MapObjective{ - public int x, y; + public Point2 pos = new Point2(); public Team team = Team.crux; - public Block block = Blocks.router; + public @Synthetic Block block = Blocks.router; public DestroyBlockObjective(Block block, int x, int y, Team team){ this.block = block; - this.x = x; - this.y = y; this.team = team; + this.pos.set(x, y); } - public DestroyBlockObjective(){ - } + public DestroyBlockObjective(){} @Override - public boolean complete(){ - var build = world.build(x, y); + public boolean update(){ + var build = world.build(pos.x, pos.y); return build == null || build.team != team || build.block != block; } @@ -275,25 +506,22 @@ public class MapObjectives{ } public static class DestroyBlocksObjective extends MapObjective{ - public int[] positions = {}; + public @Unordered Point2[] positions = {}; public Team team = Team.crux; - public Block block = Blocks.router; + public @Synthetic Block block = Blocks.router; - public DestroyBlocksObjective(Block block, Team team, int... positions){ + public DestroyBlocksObjective(Block block, Team team, Point2... positions){ this.block = block; this.team = team; this.positions = positions; } - public DestroyBlocksObjective(){ - } + public DestroyBlocksObjective(){} public int progress(){ int count = 0; for(var pos : positions){ - int x = Point2.x(pos), y = Point2.y(pos); - - var build = world.build(x, y); + var build = world.build(pos.x, pos.y); if(build == null || build.team != team || build.block != block){ count ++; } @@ -302,7 +530,7 @@ public class MapObjectives{ } @Override - public boolean complete(){ + public boolean update(){ return progress() >= positions.length; } @@ -314,16 +542,15 @@ public class MapObjectives{ /** Command any unit to do anything. Always compete in headless mode. */ public static class CommandModeObjective extends MapObjective{ + @Override + public boolean update(){ + return headless || control.input.selectedUnits.contains(u -> u.isCommandable() && u.command().hasCommand()); + } @Override public String text(){ return Core.bundle.get("objective.command"); } - - @Override - public boolean complete(){ - return headless || control.input.selectedUnits.contains(u -> u.isCommandable() && u.command().hasCommand()); - } } /** Wait until a logic flag is set. */ @@ -335,189 +562,138 @@ public class MapObjectives{ this.text = text; } - public FlagObjective(){ + public FlagObjective(){} + + @Override + public boolean update(){ + return state.rules.objectiveFlags.contains(flag); } @Override public String text(){ return text != null && text.startsWith("@") ? Core.bundle.get(text.substring(1)) : text; } - - @Override - public boolean complete(){ - return state.rules.objectiveFlags.contains(flag); - } } /** Destroy all enemy core(s). */ public static class DestroyCoreObjective extends MapObjective{ + @Override + public boolean update(){ + return state.rules.waveTeam.cores().size == 0; + } @Override public String text(){ return Core.bundle.get("objective.destroycore"); } - - @Override - public boolean complete(){ - return state.rules.waveTeam.cores().size == 0; - } } - /** Base abstract class for any in-map objective. */ - public static abstract class MapObjective{ - public @Nullable String details; - public String[] flagsAdded = {}; - public String[] flagsRemoved = {}; - public ObjectiveMarker[] markers = {}; + /** Marker used for drawing UI to indicate something along with an objective. */ + public static abstract class ObjectiveMarker{ + /** Makes sure markers are only added once. */ + private transient boolean wasAdded; - //TODO localize + /** Called in the overlay draw layer.*/ + public void draw(){} + /** Called in the small and large map. */ + public void drawMinimap(MinimapRenderer minimap){} + /** Add any UI elements necessary. */ + public void added(){} + /** Remove any UI elements, if necessary. */ + public void removed(){} + + /** @return The localized type-name of this objective, defaulting to the class simple name without the "Marker" prefix. */ public String typeName(){ - return getClass().getSimpleName().replace("Objective", ""); - } - - public MapObjective withFlags(String... flags){ - this.flagsAdded = flags; - return this; - } - - public MapObjective withFlagsRemoved(String... flags){ - this.flagsRemoved = flags; - return this; - } - - public MapObjective withMarkers(ObjectiveMarker... markers){ - this.markers = markers; - return this; - } - - public MapObjective withDetails(String details){ - this.details = details; - return this; - } - - public boolean complete(){ - return false; - } - - /** Called immediately after this objective is completed and removed from the rules. */ - public void completed(){ - - } - - public void update(){ - - } - - /** Reset internal state, if any. */ - public void reset(){ - - } - - /** Basic mission display text. If null, falls back to standard text. */ - public @Nullable String text(){ - return null; - } - - /** Details that appear upon click. */ - public @Nullable String details(){ - return details; + String className = getClass().getSimpleName().replace("Marker", ""); + return Core.bundle == null ? className : Core.bundle.get("marker." + className.toLowerCase() + ".name", className); } } /** Displays text above a shape. */ public static class ShapeTextMarker extends ObjectiveMarker{ public String text = "frog"; - public float x, y, fontSize = 1f, textHeight = 7f; - public byte flags = WorldLabel.flagBackground | WorldLabel.flagOutline; + public @TilePos Vec2 pos = new Vec2(); + public float fontSize = 1f, textHeight = 7f; + public @LabelFlag byte flags = WorldLabel.flagBackground | WorldLabel.flagOutline; public float radius = 6f, rotation = 0f; public int sides = 4; public Color color = Color.valueOf("ffd37f"); - //cached localized text + // Cached localized text. private transient String fetchedText; public ShapeTextMarker(String text, float x, float y){ this.text = text; - this.x = x; - this.y = y; + this.pos.set(x, y); } public ShapeTextMarker(String text, float x, float y, float radius){ this.text = text; - this.x = x; - this.y = y; + this.pos.set(x, y); this.radius = radius; } public ShapeTextMarker(String text, float x, float y, float radius, float rotation){ this.text = text; - this.x = x; - this.y = y; + this.pos.set(x, y); this.radius = radius; this.rotation = rotation; } public ShapeTextMarker(String text, float x, float y, float radius, float rotation, float textHeight){ this.text = text; - this.x = x; - this.y = y; + this.pos.set(x, y); this.radius = radius; this.rotation = rotation; this.textHeight = textHeight; } - public ShapeTextMarker(){ - - } + public ShapeTextMarker(){} @Override public void draw(){ Lines.stroke(3f, Pal.gray); - Lines.poly(x, y, sides, radius + 1f, rotation); + Lines.poly(pos.x, pos.y, sides, radius + 1f, rotation); Lines.stroke(1f, color); - Lines.poly(x, y, sides, radius + 1f, rotation); + Lines.poly(pos.x, pos.y, sides, radius + 1f, rotation); Draw.reset(); if(fetchedText == null){ fetchedText = text.startsWith("@") ? Core.bundle.get(text.substring(1)) : text; } - WorldLabel.drawAt(text, x, y + radius + textHeight, Draw.z(), flags, fontSize); + WorldLabel.drawAt(text, pos.x, pos.y + radius + textHeight, Draw.z(), flags, fontSize); } } /** Displays a circle on the minimap. */ public static class MinimapMarker extends ObjectiveMarker{ - public int x, y; + public Point2 pos = new Point2(); public float radius = 5f, stroke = 11f; public Color color = Color.valueOf("f25555"); public MinimapMarker(int x, int y){ - this.x = x; - this.y = y; + this.pos.set(x, y); } public MinimapMarker(int x, int y, Color color){ - this.x = x; - this.y = y; + this.pos.set(x, y); this.color = color; } public MinimapMarker(int x, int y, float radius, float stroke, Color color){ - this.x = x; - this.y = y; + this.pos.set(x, y); this.stroke = stroke; this.radius = radius; this.color = color; } - public MinimapMarker(){ - } + public MinimapMarker(){} @Override public void drawMinimap(MinimapRenderer minimap){ - minimap.transform(Tmp.v1.set(x * tilesize, y * tilesize)); + minimap.transform(Tmp.v1.set(pos.x * tilesize, pos.y * tilesize)); float rad = minimap.scale(radius * tilesize); float fin = Interp.pow2Out.apply((Time.globalTime / 100f) % 1f); @@ -530,25 +706,23 @@ public class MapObjectives{ /** Displays a shape with an outline and color. */ public static class ShapeMarker extends ObjectiveMarker{ - public float x, y, radius = 8f, rotation = 0f, stroke = 1f; + public @TilePos Vec2 pos = new Vec2(); + public float radius = 8f, rotation = 0f, stroke = 1f; public boolean fill = false, outline = true; public int sides = 4; public Color color = Color.valueOf("ffd37f"); public ShapeMarker(float x, float y){ - this.x = x; - this.y = y; + this.pos.set(x, y); } public ShapeMarker(float x, float y, float radius, float rotation){ - this.x = x; - this.y = y; + this.pos.set(x, y); this.radius = radius; this.rotation = rotation; } - public ShapeMarker(){ - } + public ShapeMarker(){} @Override public void draw(){ @@ -558,14 +732,14 @@ public class MapObjectives{ if(!fill){ if(outline){ Lines.stroke(stroke + 2f, Pal.gray); - Lines.poly(x, y, sides, radius + 1f, rotation); + Lines.poly(pos.x, pos.y, sides, radius + 1f, rotation); } Lines.stroke(stroke, color); - Lines.poly(x, y, sides, radius + 1f, rotation); + Lines.poly(pos.x, pos.y, sides, radius + 1f, rotation); }else{ Draw.color(color); - Fill.poly(x, y, sides, radius); + Fill.poly(pos.x, pos.y, sides, radius); } Draw.reset(); @@ -575,27 +749,25 @@ public class MapObjectives{ /** Displays text at a location. */ public static class TextMarker extends ObjectiveMarker{ public String text = "uwu"; - public float x, y, fontSize = 1f; - public byte flags = WorldLabel.flagBackground | WorldLabel.flagOutline; - //cached localized text + public @TilePos Vec2 pos = new Vec2(); + public float fontSize = 1f; + public @LabelFlag byte flags = WorldLabel.flagBackground | WorldLabel.flagOutline; + // Cached localized text. private transient String fetchedText; public TextMarker(String text, float x, float y, float fontSize, byte flags){ this.text = text; - this.x = x; - this.y = y; this.fontSize = fontSize; this.flags = flags; + this.pos.set(x, y); } public TextMarker(String text, float x, float y){ this.text = text; - this.x = x; - this.y = y; + this.pos.set(x, y); } - public TextMarker(){ - } + public TextMarker(){} @Override public void draw(){ @@ -603,27 +775,37 @@ public class MapObjectives{ fetchedText = text.startsWith("@") ? Core.bundle.get(text.substring(1)) : text; } - WorldLabel.drawAt(fetchedText, x, y, Draw.z(), flags, fontSize); + WorldLabel.drawAt(fetchedText, pos.x, pos.y, Draw.z(), flags, fontSize); } } - /** Marker used for drawing UI to indicate something along with an objective. */ - public static abstract class ObjectiveMarker{ - /** makes sure markers are only added once */ - public transient boolean wasAdded; + /** For arrays or {@link Seq}s; does not create element rearrangement buttons. */ + @Target(FIELD) + @Retention(RUNTIME) + public @interface Unordered{} - //TODO localize - public String typeName(){ - return getClass().getSimpleName().replace("Marker", ""); - } + /** For {@code byte}; treats it as a world label flag. */ + @Target(FIELD) + @Retention(RUNTIME) + public @interface LabelFlag{} - /** Called in the overlay draw layer.*/ - public void draw(){} - /** Called in the small & large map. */ - public void drawMinimap(MinimapRenderer minimap){} - /** Add any UI elements necessary. */ - public void added(){} - /** Remove any UI elements, if necessary. */ - public void removed(){} - } + /** For {@link UnlockableContent}; filters all un-researchable content. */ + @Target(FIELD) + @Retention(RUNTIME) + public @interface Researchable{} + + /** For {@link Block}; filters all un-buildable blocks. */ + @Target(FIELD) + @Retention(RUNTIME) + public @interface Synthetic{} + + /** For {@code float}; multiplies the UI input by 60. */ + @Target(FIELD) + @Retention(RUNTIME) + public @interface Second{} + + /** For {@code float} or similar data structures, such as {@link Vec2}; multiplies the UI input by {@link Vars#tilesize}. */ + @Target(FIELD) + @Retention(RUNTIME) + public @interface TilePos{} } diff --git a/core/src/mindustry/game/Rules.java b/core/src/mindustry/game/Rules.java index bee8e83f99..3e33b3231c 100644 --- a/core/src/mindustry/game/Rules.java +++ b/core/src/mindustry/game/Rules.java @@ -7,7 +7,6 @@ import arc.util.serialization.*; import arc.util.serialization.Json.*; import mindustry.*; import mindustry.content.*; -import mindustry.game.MapObjectives.*; import mindustry.graphics.g3d.*; import mindustry.io.*; import mindustry.type.*; @@ -124,9 +123,9 @@ public class Rules{ public ObjectSet researched = new ObjectSet<>(); /** Block containing these items as requirements are hidden. */ public ObjectSet hiddenBuildItems = Items.erekirOnlyItems.asSet(); - /** Campaign-only map objectives. */ - public Seq objectives = new Seq<>(); - /** Flags set by objectives. Used in world processors. n*/ + /** In-map objective executor. */ + public MapObjectives objectives = new MapObjectives(); + /** Flags set by objectives. Used in world processors. */ public ObjectSet objectiveFlags = new ObjectSet<>(); /** If true, fog of war is enabled. Enemy units and buildings are hidden unless in radar view. */ public boolean fog = false; diff --git a/core/src/mindustry/graphics/MinimapRenderer.java b/core/src/mindustry/graphics/MinimapRenderer.java index 93871e917b..3636c0c342 100644 --- a/core/src/mindustry/graphics/MinimapRenderer.java +++ b/core/src/mindustry/graphics/MinimapRenderer.java @@ -190,12 +190,9 @@ public class MinimapRenderer{ drawSpawns(x, y, w, h, scaling); } - if(state.rules.objectives.size > 0){ - var first = state.rules.objectives.first(); - for(var marker : first.markers){ - marker.drawMinimap(this); - } - } + state.rules.objectives.eachRunning(obj -> { + for(var marker : obj.markers) marker.drawMinimap(this); + }); } public void drawSpawns(float x, float y, float w, float h, float scaling){ diff --git a/core/src/mindustry/graphics/OverlayRenderer.java b/core/src/mindustry/graphics/OverlayRenderer.java index 265761e0f6..cd452ca24a 100644 --- a/core/src/mindustry/graphics/OverlayRenderer.java +++ b/core/src/mindustry/graphics/OverlayRenderer.java @@ -113,13 +113,10 @@ public class OverlayRenderer{ } } - //draw objective markers, if any - if(state.rules.objectives.size > 0){ - var first = state.rules.objectives.first(); - for(var marker : first.markers){ - marker.draw(); - } - } + //draw objective markers + state.rules.objectives.eachRunning(obj -> { + for(var marker : obj.markers) marker.draw(); + }); if(player.dead()) return; //dead players don't draw diff --git a/core/src/mindustry/io/JsonIO.java b/core/src/mindustry/io/JsonIO.java index 784dfbc583..d594cb3218 100644 --- a/core/src/mindustry/io/JsonIO.java +++ b/core/src/mindustry/io/JsonIO.java @@ -1,6 +1,7 @@ package mindustry.io; import arc.graphics.*; +import arc.math.geom.*; import arc.util.*; import arc.util.serialization.*; import arc.util.serialization.Json.*; @@ -8,6 +9,7 @@ import mindustry.*; import mindustry.content.*; import mindustry.ctype.*; import mindustry.game.*; +import mindustry.game.MapObjectives.*; import mindustry.maps.*; import mindustry.type.*; import mindustry.world.*; @@ -24,9 +26,9 @@ public class JsonIO{ @Override public void writeValue(Object value, Class knownType, Class elementType){ - if(value instanceof MappableContent){ + if(value instanceof MappableContent c){ try{ - getWriter().value(((MappableContent)value).name); + getWriter().value(c.name); }catch(IOException e){ throw new RuntimeException(e); } @@ -37,9 +39,7 @@ public class JsonIO{ @Override protected String convertToString(Object object){ - if(object instanceof MappableContent){ - return ((MappableContent)object).name; - } + if(object instanceof MappableContent c) return c.name; return super.convertToString(object); } }; @@ -69,6 +69,11 @@ public class JsonIO{ return json.prettyPrint(in); } + public static void classTag(String tag, Class type){ + json.addClassTag(tag, type); + jsonBase.addClassTag(tag, type); + } + static void apply(Json json){ json.setElementType(Rules.class, "spawns", SpawnGroup.class); json.setElementType(Rules.class, "loadout", ItemStack.class); @@ -255,23 +260,61 @@ public class JsonIO{ } }); + json.setSerializer(MapObjectives.class, new Serializer<>(){ + @Override + public void write(Json json, MapObjectives exec, Class knownType){ + json.writeArrayStart(); + for(var obj : exec){ + json.writeObjectStart(obj.getClass().isAnonymousClass() ? obj.getClass().getSuperclass() : obj.getClass(), null); + json.writeFields(obj); + + json.writeArrayStart("parents"); + for(var parent : obj.parents){ + json.writeValue(exec.all.indexOf(parent)); + } + + json.writeArrayEnd(); + + json.writeValue("editorPos", Point2.pack(obj.editorX, obj.editorY)); + json.writeObjectEnd(); + } + + json.writeArrayEnd(); + } + + @Override + public MapObjectives read(Json json, JsonValue data, Class type){ + var exec = new MapObjectives(); + // First iteration to instantiate the objectives. + for(var value = data.child; value != null; value = value.next){ + MapObjective obj = json.readValue(MapObjective.class, value); + + int pos = value.getInt("editorPos"); + obj.editorX = Point2.x(pos); + obj.editorY = Point2.y(pos); + + exec.all.add(obj); + } + + // Second iteration to map the parents. + int i = 0; + for(var value = data.child; value != null; value = value.next, i++){ + for(var parent = value.get("parents").child; parent != null; parent = parent.next){ + exec.all.get(i).parents.add(exec.all.get(parent.asInt())); + } + } + + return exec; + } + }); + + + //use short names for all filter types for(var filter : Maps.allFilterTypes){ var i = filter.get(); json.addClassTag(Strings.camelize(i.getClass().getSimpleName().replace("Filter", "")), i.getClass()); } - - //use short names for all objective types - for(var obj : MapObjectives.allObjectiveTypes){ - var i = obj.get(); - json.addClassTag(Strings.camelize(i.getClass().getSimpleName().replace("Objective", "")), i.getClass()); - } - - //use short names for all marker types - for(var obj : MapObjectives.allMarkerTypes){ - var i = obj.get(); - json.addClassTag(Strings.camelize(i.getClass().getSimpleName().replace("Marker", "")), i.getClass()); - } } static class CustomJson extends Json{ diff --git a/core/src/mindustry/io/TypeIO.java b/core/src/mindustry/io/TypeIO.java index 226fc67ea6..13887ed269 100644 --- a/core/src/mindustry/io/TypeIO.java +++ b/core/src/mindustry/io/TypeIO.java @@ -546,6 +546,19 @@ public class TypeIO{ return JsonIO.read(Rules.class, string); } + public static void writeObjectives(Writes write, MapObjectives executor){ + String string = JsonIO.write(executor); + byte[] bytes = string.getBytes(charset); + write.i(bytes.length); + write.b(bytes); + } + + public static MapObjectives readObjectives(Reads read){ + int length = read.i(); + String string = new String(read.b(new byte[length]), charset); + return JsonIO.read(MapObjectives.class, string); + } + public static void writeVecNullable(Writes write, @Nullable Vec2 v){ if(v == null){ write.f(Float.NaN); diff --git a/core/src/mindustry/ui/fragments/HudFragment.java b/core/src/mindustry/ui/fragments/HudFragment.java index f9c564d087..dde7340a41 100644 --- a/core/src/mindustry/ui/fragments/HudFragment.java +++ b/core/src/mindustry/ui/fragments/HudFragment.java @@ -775,13 +775,21 @@ public class HudFragment{ builder.setLength(0); //objectives override mission? - if(state.rules.objectives.size > 0){ - var first = state.rules.objectives.first(); - String text = first.text(); - if(text != null){ - builder.append(text); - return builder; + if(state.rules.objectives.any()){ + boolean first = true; + for(var obj : state.rules.objectives){ + if(!obj.qualified()) continue; + + String text = obj.text(); + if(text != null){ + if(!first) builder.append('\n'); + builder.append(text); + + first = false; + } } + + return builder; } //mission overrides everything @@ -832,13 +840,24 @@ public class HudFragment{ table.row(); table.clicked(() -> { - if(state.rules.objectives.size > 0){ - var first = state.rules.objectives.first(); - var details = first.details(); - if(details != null){ - //TODO this could be much better. - ui.showInfo(details); + if(state.rules.objectives.any()){ + StringBuilder text = new StringBuilder(); + + boolean first = true; + for(var obj : state.rules.objectives){ + if(!obj.qualified()) continue; + + String details = obj.details(); + if(details != null){ + if(!first) text.append('\n'); + text.append(details); + + first = false; + } } + + //TODO this, as said before, could be much better. + ui.showInfo(text.toString()); } });