1
0
mirror of https://github.com/Anuken/Mindustry.git synced 2024-09-11 08:15:35 +03:00

Tree-like objective node structure (#7152)

* overall structure

* overall layout

* field interpreter

* less bloated UI

* scroll pan't

* strip off the 'Marker' suffix

* e

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

* onset

* two, four

* i don't understand how icon mappings work.

* separate remover and indexer

* some cleanups

* untested mobile support

* contrib

* ok anuke

* fix conflicts 2

* hidden
This commit is contained in:
GlennFolker 2022-07-15 21:41:18 +07:00 committed by GitHub
parent 3b1c8baca9
commit 695c19d0b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1733 additions and 810 deletions

View File

@ -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/**")

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

View File

@ -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

View File

@ -146,3 +146,4 @@ Alex25820
KayAyeAre
SMOLKEYS
1stvaliduser(SUS)
GlennFolker

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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

View File

@ -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){

View File

@ -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();

View File

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

View File

@ -0,0 +1,570 @@
package mindustry.editor;
import arc.*;
import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.input.*;
import arc.math.*;
import arc.math.geom.*;
import arc.scene.event.*;
import arc.scene.ui.*;
import arc.scene.ui.ImageButton.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.editor.MapObjectivesCanvas.ObjectiveTilemap.ObjectiveTile.*;
import mindustry.editor.MapObjectivesDialog.*;
import mindustry.game.MapObjectives.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.ui.*;
import mindustry.ui.dialogs.*;
import static mindustry.Vars.*;
@SuppressWarnings("unchecked")
public class MapObjectivesCanvas extends WidgetGroup{
public static final int
objWidth = 5, objHeight = 2,
bounds = 100;
public static final float unitSize = 48f;
public Seq<MapObjective> objectives = new Seq<>();
public ObjectiveTilemap tilemap;
protected MapObjective query;
private boolean pressed;
private long visualPressed;
private int queryX = -objWidth, queryY = -objHeight;
public MapObjectivesCanvas(){
setFillParent(true);
addChild(tilemap = new ObjectiveTilemap());
addCaptureListener(new InputListener(){
@Override
public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){
if(query != null && button == KeyCode.mouseRight){
stopQuery();
event.stop();
return true;
}else{
return false;
}
}
});
addCaptureListener(new ElementGestureListener(){
int pressPointer = -1;
@Override
public void pan(InputEvent event, float x, float y, float deltaX, float deltaY){
if(tilemap.moving != null || tilemap.connecting != null) return;
tilemap.x = Mathf.clamp(tilemap.x + deltaX, -bounds * unitSize + width, 0f);
tilemap.y = Mathf.clamp(tilemap.y + deltaY, -bounds * unitSize + height, 0f);
}
@Override
public void tap(InputEvent event, float x, float y, int count, KeyCode button){
if(query == null) return;
Vec2 pos = localToDescendantCoordinates(tilemap, Tmp.v1.set(x, y));
queryX = Mathf.round((pos.x - objWidth * unitSize / 2f) / unitSize);
queryY = Mathf.floor((pos.y - unitSize) / unitSize);
// In mobile, placing the query is done in a separate button.
if(!mobile) placeQuery();
}
@Override
public void touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){
if(pressPointer != -1) return;
pressPointer = pointer;
pressed = true;
visualPressed = Time.millis() + 100;
}
@Override
public void touchUp(InputEvent event, float x, float y, int pointer, KeyCode button){
if(pointer == pressPointer){
pressPointer = -1;
pressed = false;
}
}
});
}
public void clearObjectives(){
stopQuery();
tilemap.clearTiles();
}
protected void stopQuery(){
if(query == null) return;
query = null;
Core.graphics.restoreCursor();
}
public void query(MapObjective obj){
stopQuery();
query = obj;
}
public void placeQuery(){
if(isQuerying() && tilemap.createTile(queryX, queryY, query)){
objectives.add(query);
stopQuery();
}
}
public boolean isQuerying(){
return query != null;
}
public boolean isVisualPressed(){
return pressed || visualPressed > Time.millis();
}
public class ObjectiveTilemap extends WidgetGroup{
protected final GridBits grid = new GridBits(bounds, bounds);
/** The connector button that is being pressed. */
protected @Nullable Connector connecting;
/** The current tile that is being moved. */
protected @Nullable ObjectiveTile moving;
public ObjectiveTilemap(){
setTransform(false);
setSize(getPrefWidth(), getPrefHeight());
touchable(() -> isQuerying() ? Touchable.disabled : Touchable.childrenOnly);
}
@Override
public void draw(){
validate();
int minX = Math.max(Mathf.floor((x - 1f) / unitSize), 0), minY = Math.max(Mathf.floor((y - 1f) / unitSize), 0),
maxX = Math.min(Mathf.ceil((x + width + 1f) / unitSize), bounds), maxY = Math.min(Mathf.ceil((y + height + 1f) / unitSize), bounds);
float progX = x % unitSize, progY = y % unitSize;
Lines.stroke(2f);
Draw.color(Pal.gray, parentAlpha);
for(int x = minX; x <= maxX; x++) Lines.line(progX + x * unitSize, minY * unitSize, progX + x * unitSize, maxY * unitSize);
for(int y = minY; y <= maxY; y++) Lines.line(minX * unitSize, progY + y * unitSize, maxX * unitSize, progY + y * unitSize);
if(isQuerying()){
int tx, ty;
if(mobile){
tx = queryX;
ty = queryY;
}else{
Vec2 pos = screenToLocalCoordinates(Core.input.mouse());
tx = Mathf.round((pos.x - objWidth * unitSize / 2f) / unitSize);
ty = Mathf.floor((pos.y - unitSize) / unitSize);
}
Lines.stroke(4f);
Draw.color(
isVisualPressed() ? Pal.metalGrayDark : validPlace(tx, ty) ? Pal.accent : Pal.remove,
parentAlpha * (inPlaceBounds(tx, ty) ? 1f : Mathf.absin(3f, 1f))
);
Lines.rect(x + tx * unitSize, y + ty * unitSize, objWidth * unitSize, objHeight * unitSize);
}
if(moving != null){
int tx, ty;
float x = this.x + (tx = Mathf.round(moving.x / unitSize)) * unitSize;
float y = this.y + (ty = Mathf.round(moving.y / unitSize)) * unitSize;
Draw.color(
validMove(moving, tx, ty) ? Pal.accent : Pal.remove,
0.5f * parentAlpha * (inPlaceBounds(tx, ty) ? 1f : Mathf.absin(3f, 1f))
);
Fill.crect(x, y, objWidth * unitSize, objHeight * unitSize);
}
Draw.reset();
super.draw();
Draw.reset();
Seq<ObjectiveTile> tiles = getChildren().as();
Connector conTarget = null;
if(connecting != null){
Vec2 pos = connecting.localToAscendantCoordinates(this, Tmp.v1.set(connecting.pointX, connecting.pointY));
if(hit(pos.x, pos.y, true) instanceof Connector con && connecting.canConnectTo(con)) conTarget = con;
}
boolean removing = false;
for(var tile : tiles){
for(var parent : tile.obj.parents){
var parentTile = tiles.find(t -> t.obj == parent);
Connector
conFrom = parentTile.conChildren,
conTo = tile.conParent;
if(conTarget != null && (
(connecting.findParent && connecting == conTo && conTarget == conFrom) ||
(!connecting.findParent && connecting == conFrom && conTarget == conTo)
)){
removing = true;
continue;
}
Vec2
from = conFrom.localToAscendantCoordinates(this, Tmp.v1.set(conFrom.getWidth() / 2f, conFrom.getHeight() / 2f)).add(x, y),
to = conTo.localToAscendantCoordinates(this, Tmp.v2.set(conTo.getWidth() / 2f, conTo.getHeight() / 2f)).add(x, y);
drawCurve(false, from.x, from.y, to.x, to.y);
}
}
if(connecting != null){
Vec2
mouse = (conTarget == null
? connecting.localToAscendantCoordinates(this, Tmp.v1.set(connecting.pointX, connecting.pointY))
: conTarget.localToAscendantCoordinates(this, Tmp.v1.set(conTarget.getWidth() / 2f, conTarget.getHeight() / 2f))
).add(x, y),
anchor = connecting.localToAscendantCoordinates(this, Tmp.v2.set(connecting.getWidth() / 2f, connecting.getHeight() / 2f)).add(x, y);
Vec2
from = connecting.findParent ? mouse : anchor,
to = connecting.findParent ? anchor : mouse;
drawCurve(removing, from.x, from.y, to.x, to.y);
}
Draw.reset();
}
protected void drawCurve(boolean remove, float x1, float y1, float x2, float y2){
Lines.stroke(4f);
Draw.color(remove ? Pal.remove : Pal.accent, parentAlpha);
float dist = Math.abs(x1 - x2) / 2f;
Lines.curve(x1, y1, x1 + dist, y1, x2 - dist, y2, x2, y2, Math.max(4, (int) (Mathf.dst(x1, y1, x2, y2) / 4f)));
Draw.reset();
}
public boolean inPlaceBounds(int x, int y){
return Structs.inBounds(x, y, bounds - objWidth + 1, bounds - objHeight + 1);
}
public boolean validPlace(int x, int y){
if(!inPlaceBounds(x, y)) return false;
for(int tx = 0; tx < objWidth; tx++){
for(int ty = 0; ty < objHeight; ty++){
if(occupied(x + tx, y + ty)) return false;
}
}
return true;
}
public boolean validMove(ObjectiveTile tile, int newX, int newY){
if(!inPlaceBounds(newX, newY)) return false;
int x = tile.tx, y = tile.ty;
for(int tx = 0; tx < objWidth; tx++){
for(int ty = 0; ty < objHeight; ty++){
grid.set(x + tx, y + ty, false);
}
}
boolean valid = validPlace(newX, newY);
for(int tx = 0; tx < objWidth; tx++){
for(int ty = 0; ty < objHeight; ty++){
grid.set(x + tx, y + ty);
}
}
return valid;
}
public boolean occupied(int x, int y){
return grid.get(x, y);
}
public boolean createTile(MapObjective obj){
return createTile(obj.editorX, obj.editorY, obj);
}
public boolean createTile(int x, int y, MapObjective obj){
if(!validPlace(x, y)) return false;
ObjectiveTile tile = new ObjectiveTile(obj, x, y);
tile.pack();
addChild(tile);
for(int tx = 0; tx < objWidth; tx++){
for(int ty = 0; ty < objHeight; ty++){
grid.set(x + tx, y + ty);
}
}
return true;
}
public boolean moveTile(ObjectiveTile tile, int newX, int newY){
if(!validMove(tile, newX, newY)) return false;
int x = tile.tx, y = tile.ty;
for(int tx = 0; tx < objWidth; tx++){
for(int ty = 0; ty < objHeight; ty++){
grid.set(x + tx, y + ty, false);
}
}
tile.pos(newX, newY);
x = newX;
y = newY;
for(int tx = 0; tx < objWidth; tx++){
for(int ty = 0; ty < objHeight; ty++){
grid.set(x + tx, y + ty);
}
}
return true;
}
public void removeTile(ObjectiveTile tile){
if(!tile.isDescendantOf(this)) return;
tile.remove();
int x = tile.tx, y = tile.ty;
for(int tx = 0; tx < objWidth; tx++){
for(int ty = 0; ty < objHeight; ty++){
grid.set(x + tx, y + ty, false);
}
}
}
public void clearTiles(){
clearChildren();
grid.clear();
}
@Override
public float getPrefWidth(){
return bounds * unitSize;
}
@Override
public float getPrefHeight(){
return bounds * unitSize;
}
public class ObjectiveTile extends Table{
public final MapObjective obj;
public int tx, ty;
public final Mover mover;
public final Connector conParent, conChildren;
public ObjectiveTile(MapObjective obj, int x, int y){
this.obj = obj;
setTransform(false);
setClip(false);
add(conParent = new Connector(true)).size(unitSize);
add(new ImageButton(Icon.move, new ImageButtonStyle(){{
up = Tex.whiteui;
imageUpColor = Color.black;
}})).color(Pal.accent).height(unitSize).growX().get().addCaptureListener(mover = new Mover());
add(conChildren = new Connector(false)).size(unitSize);
row().table(Tex.buttonSelectTrans, t -> {
t.labelWrap(obj.typeName()).grow()
.color(Pal.accent).align(Align.left).padLeft(6f)
.ellipsis(true).get().setAlignment(Align.left);
t.table(b -> {
b.right().defaults().size(32f).pad((unitSize - 32f) / 2f - 4f);
b.button(Icon.pencilSmall, () -> {
BaseDialog dialog = new BaseDialog("@editor.objectives");
dialog.cont.pane(Styles.noBarPane, list -> list.top().table(e -> {
e.margin(0f);
MapObjectivesDialog.getInterpreter((Class<MapObjective>)obj.getClass()).build(
e, obj.typeName(), new TypeInfo(obj.getClass()),
null, null, null,
() -> obj,
res -> {}
);
}).width(400f).fillY()).grow();
dialog.addCloseButton();
dialog.show();
});
b.button(Icon.trashSmall, () -> removeTile(this));
}).growY().fillX();
}).grow().colspan(3);
setSize(getPrefWidth(), getPrefHeight());
pos(x, y);
}
public void pos(int x, int y){
tx = obj.editorX = x;
ty = obj.editorY = y;
this.x = x * unitSize;
this.y = y * unitSize;
}
@Override
public float getPrefWidth(){
return objWidth * unitSize;
}
@Override
public float getPrefHeight(){
return objHeight * unitSize;
}
@Override
public boolean remove(){
if(super.remove()){
obj.parents.clear();
var it = objectives.iterator();
while(it.hasNext()){
var next = it.next();
if(next == obj){
it.remove();
}else{
next.parents.remove(obj);
}
}
return true;
}else{
return false;
}
}
public class Mover extends InputListener{
public int prevX, prevY;
public float lastX, lastY;
@Override
public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){
if(moving != null) return false;
moving = ObjectiveTile.this;
moving.toFront();
prevX = moving.tx;
prevY = moving.ty;
// Convert to world pos first because the button gets dragged too.
Vec2 pos = event.listenerActor.localToStageCoordinates(Tmp.v1.set(x, y));
lastX = pos.x;
lastY = pos.y;
return true;
}
@Override
public void touchDragged(InputEvent event, float x, float y, int pointer){
Vec2 pos = event.listenerActor.localToStageCoordinates(Tmp.v1.set(x, y));
moving.moveBy(pos.x - lastX, pos.y - lastY);
lastX = pos.x;
lastY = pos.y;
}
@Override
public void touchUp(InputEvent event, float x, float y, int pointer, KeyCode button){
if(!moveTile(moving,
Mathf.round(moving.x / unitSize),
Mathf.round(moving.y / unitSize)
)) moving.pos(prevX, prevY);
moving = null;
}
}
public class Connector extends Button{
public float pointX, pointY;
public final boolean findParent;
public Connector(boolean findParent){
super(new ButtonStyle(){{
down = findParent ? Tex.buttonEdgeDown1 : Tex.buttonEdgeDown3;
up = findParent ? Tex.buttonEdge1 : Tex.buttonEdge3;
over = findParent ? Tex.buttonEdgeOver1 : Tex.buttonEdgeOver3;
}});
this.findParent = findParent;
clearChildren();
addCaptureListener(new InputListener(){
int conPointer = -1;
@Override
public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){
if(conPointer != -1) return false;
conPointer = pointer;
if(connecting != null) return false;
connecting = Connector.this;
pointX = x;
pointY = y;
return true;
}
@Override
public void touchDragged(InputEvent event, float x, float y, int pointer){
if(conPointer != pointer) return;
pointX = x;
pointY = y;
}
@Override
public void touchUp(InputEvent event, float x, float y, int pointer, KeyCode button){
if(conPointer != pointer || connecting != Connector.this) return;
conPointer = -1;
Vec2 pos = Connector.this.localToAscendantCoordinates(ObjectiveTilemap.this, Tmp.v1.set(x, y));
if(ObjectiveTilemap.this.hit(pos.x, pos.y, true) instanceof Connector con && con.canConnectTo(Connector.this)){
if(findParent){
if(!obj.parents.remove(con.tile().obj)) obj.parents.add(con.tile().obj);
}else{
if(!con.tile().obj.parents.remove(obj)) con.tile().obj.parents.add(obj);
}
}
connecting = null;
}
});
}
public boolean canConnectTo(Connector other){
return
findParent != other.findParent &&
tile() != other.tile();
}
public ObjectiveTile tile(){
return ObjectiveTile.this;
}
@Override
public boolean isPressed(){
return super.isPressed() || connecting == this;
}
@Override
public boolean isOver(){
return super.isOver() && (connecting == null || connecting.canConnectTo(this));
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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<MapObjective>[] 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<MapObjective>, Eachable<MapObjective>{
public static final Seq<Prov<? extends MapObjective>> allObjectiveTypes = new Seq<>();
public static final Seq<Prov<? extends ObjectiveMarker>> allMarkerTypes = new Seq<>();
public static Prov<ObjectiveMarker>[] 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<MapObjective> 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<? extends MapObjective>... providers){
for(var prov : providers){
allObjectiveTypes.add(prov);
Class<? extends MapObjective> type = prov.get().getClass();
JsonIO.classTag(type.getSimpleName().replace("Objective", ""), type);
}
}
@SafeVarargs
public static void registerMarker(Prov<? extends ObjectiveMarker>... providers){
for(var prov : providers){
allMarkerTypes.add(prov);
Class<? extends ObjectiveMarker> 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<MapObjective> cons){
all.each(MapObjective::qualified, cons);
}
/** Iterates over all qualified in-map objectives, with a filter. */
public <T extends MapObjective> void eachRunning(Boolf<? super MapObjective> pred, Cons<T> cons){
all.each(obj -> obj.qualified() && pred.get(obj), cons);
}
@Override
public Iterator<MapObjective> iterator(){
return all.iterator();
}
@Override
public void each(Cons<? super MapObjective> 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<MapObjective> parents = new Seq<>(2);
/** Temporary container to store references since this class is static. Will immediately be flattened. */
private transient final Seq<MapObjective> 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{}
}

View File

@ -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<String> researched = new ObjectSet<>();
/** Block containing these items as requirements are hidden. */
public ObjectSet<Item> hiddenBuildItems = Items.erekirOnlyItems.asSet();
/** Campaign-only map objectives. */
public Seq<MapObjective> 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<String> objectiveFlags = new ObjectSet<>();
/** If true, fog of war is enabled. Enemy units and buildings are hidden unless in radar view. */
public boolean fog = false;

View File

@ -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){

View File

@ -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

View File

@ -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{

View File

@ -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);

View File

@ -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());
}
});