diff --git a/core/src/mindustry/entities/comp/BuildingComp.java b/core/src/mindustry/entities/comp/BuildingComp.java index 021850a20c..2c76a3bf1b 100644 --- a/core/src/mindustry/entities/comp/BuildingComp.java +++ b/core/src/mindustry/entities/comp/BuildingComp.java @@ -545,6 +545,16 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, } + /** Called when this building receives a position command. Requires a commandable block. */ + public void onCommand(Vec2 target){ + + } + + /** @return the position that this block points to for commands, or null. */ + public @Nullable Vec2 getCommandPosition(){ + return null; + } + public void handleUnitPayload(Unit unit, Cons grabber){ Fx.spawn.at(unit); diff --git a/core/src/mindustry/entities/comp/UnitComp.java b/core/src/mindustry/entities/comp/UnitComp.java index 30b45c81e8..5a43dbbfcd 100644 --- a/core/src/mindustry/entities/comp/UnitComp.java +++ b/core/src/mindustry/entities/comp/UnitComp.java @@ -281,6 +281,14 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I return controller instanceof CommandAI; } + public CommandAI command(){ + if(controller instanceof CommandAI ai){ + return ai; + }else{ + throw new IllegalArgumentException("Unit cannot be commanded - check isCommandable() first."); + } + } + public int count(){ return team.data().countType(type); } diff --git a/core/src/mindustry/entities/units/AIController.java b/core/src/mindustry/entities/units/AIController.java index 806ae8c657..4d7f9e3894 100644 --- a/core/src/mindustry/entities/units/AIController.java +++ b/core/src/mindustry/entities/units/AIController.java @@ -17,11 +17,13 @@ import static mindustry.Vars.*; public class AIController implements UnitController{ protected static final Vec2 vec = new Vec2(); + protected static final float rotateBackTimer = 60f * 5f; protected static final int timerTarget = 0, timerTarget2 = 1, timerTarget3 = 2, timerTarget4 = 3; protected Unit unit; protected Interval timer = new Interval(4); protected AIController fallback; + protected float noTargetTime; /** main target that is being faced */ protected Teamc target; @@ -128,8 +130,12 @@ public class AIController implements UnitController{ target = findMainTarget(unit.x, unit.y, unit.range(), unit.type.targetAir, unit.type.targetGround); } + noTargetTime += Time.delta; + if(invalid(target)){ target = null; + }else{ + noTargetTime = 0f; } unit.isShooting = false; @@ -168,6 +174,13 @@ public class AIController implements UnitController{ unit.isShooting |= (mount.shoot = mount.rotate = shoot); + if(mount.target == null && !shoot && !Angles.within(mount.rotation, 0f, 0.01f) && noTargetTime >= rotateBackTimer){ + mount.rotate = true; + Tmp.v1.trns(unit.rotation, 5f); + mount.aimX = mountX + Tmp.v1.x; + mount.aimY = mountY + Tmp.v1.y; + } + if(shoot){ unit.aimX = mount.aimX; unit.aimY = mount.aimY; diff --git a/core/src/mindustry/graphics/Drawf.java b/core/src/mindustry/graphics/Drawf.java index cb0bc6f44b..b9e1a9c062 100644 --- a/core/src/mindustry/graphics/Drawf.java +++ b/core/src/mindustry/graphics/Drawf.java @@ -118,6 +118,13 @@ public class Drawf{ Draw.z(pz); } + public static void limitLine(Position start, Position dest, float len1, float len2){ + Tmp.v1.set(dest).sub(start).setLength(len1); + Tmp.v2.set(Tmp.v1).scl(-1f).setLength(len2); + + Drawf.line(Pal.accent, start.getX() + Tmp.v1.x, start.getY() + Tmp.v1.y, dest.getX() + Tmp.v2.x, dest.getY() + Tmp.v2.y); + } + public static void dashLineDst(Color color, float x, float y, float x2, float y2){ dashLine(color, x, y, x2, y2, (int)(Mathf.dst(x, y, x2, y2) / tilesize * 1.6f)); } diff --git a/core/src/mindustry/input/DesktopInput.java b/core/src/mindustry/input/DesktopInput.java index 910bc7d395..ade15e1911 100644 --- a/core/src/mindustry/input/DesktopInput.java +++ b/core/src/mindustry/input/DesktopInput.java @@ -117,17 +117,12 @@ public class DesktopInput extends InputHandler{ if(commandMode){ //draw command overlay UI - for(Unit unit : selectedUnits){ - CommandAI ai = (CommandAI)unit.controller(); + CommandAI ai = unit.command(); //draw target line if(ai.targetPos != null){ Position lineDest = ai.attackTarget != null ? ai.attackTarget : ai.targetPos; - - Tmp.v1.set(lineDest).sub(unit).setLength(unit.hitSize / 2f); - Tmp.v2.set(Tmp.v1).scl(-1f).setLength(3.5f); - - Drawf.line(Pal.accent, unit.x + Tmp.v1.x, unit.y + Tmp.v1.y, lineDest.getX() + Tmp.v2.x, lineDest.getY() + Tmp.v2.y); + Drawf.limitLine(unit, lineDest, unit.hitSize / 2f, 3.5f); if(ai.attackTarget == null){ Drawf.square(lineDest.getX(), lineDest.getY(), 3.5f); @@ -141,6 +136,16 @@ public class DesktopInput extends InputHandler{ } } + if(commandBuild != null){ + Drawf.square(commandBuild.x, commandBuild.y, commandBuild.hitSize() / 1.4f + 1f); + var cpos = commandBuild.getCommandPosition(); + + if(cpos != null){ + Drawf.limitLine(commandBuild, cpos, commandBuild.hitSize() / 2f, 3.5f); + Drawf.square(cpos.x, cpos.y, 3.5f); + } + } + if(commandMode && !commandRect){ Unit sel = selectedCommandUnit(input.mouseWorldX(), input.mouseWorldY()); @@ -564,6 +569,7 @@ public class DesktopInput extends InputHandler{ selectedUnits.clear(); } selectedUnits.addAll(units); + commandBuild = null; } commandRect = false; } @@ -698,6 +704,7 @@ public class DesktopInput extends InputHandler{ //click: select a single unit if(button == KeyCode.mouseLeft){ Unit unit = selectedCommandUnit(input.mouseWorldX(), input.mouseWorldY()); + Building build = world.buildWorld(input.mouseWorldX(), input.mouseWorldY()); if(unit != null){ if(selectedUnits.contains(unit)){ selectedUnits.remove(unit); @@ -705,16 +712,24 @@ public class DesktopInput extends InputHandler{ selectedUnits.clear(); selectedUnits.add(unit); } + commandBuild = null; }else{ //deselect selectedUnits.clear(); + + if(build != null && build.team == player.team() && build.block.commandable){ + commandBuild = (commandBuild == build ? null : build); + }else{ + commandBuild = null; + } } }else if(button == KeyCode.mouseRight){ //right click: move to position + //move to location - TODO right click instead? + Vec2 target = input.mouseWorld().cpy(); + if(selectedUnits.size > 0){ - //move to location - TODO right click instead? - Vec2 target = input.mouseWorld().cpy(); Teamc attack = world.buildWorld(target.x, target.y); @@ -729,6 +744,10 @@ public class DesktopInput extends InputHandler{ Call.commandUnits(player, ids, attack instanceof Building b ? b : null, attack instanceof Unit u ? u : null, target); } + + if(commandBuild != null){ + Call.commandBuilding(player, commandBuild, target); + } } return super.tap(x, y, count, button); diff --git a/core/src/mindustry/input/InputHandler.java b/core/src/mindustry/input/InputHandler.java index 80398be577..fd22b8014b 100644 --- a/core/src/mindustry/input/InputHandler.java +++ b/core/src/mindustry/input/InputHandler.java @@ -80,6 +80,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ //for RTS controls public Seq selectedUnits = new Seq<>(); + public @Nullable Building commandBuild; public boolean commandMode = false; public boolean commandRect = false; public boolean tappedOne = false; @@ -228,6 +229,20 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ } } + @Remote(called = Loc.server, targets = Loc.both, forward = true) + public static void commandBuilding(Player player, Building build, Vec2 target){ + if(player == null || build == null || build.team != player.team() || !build.block.commandable || target == null) return; + + if(net.server() && !netServer.admins.allowAction(player, ActionType.commandBuilding, event -> { + event.tile = build.tile; + })){ + throw new ValidateException(player, "Player cannot command building."); + } + + build.onCommand(target); + Fx.moveCommand.at(target); + } + @Remote(called = Loc.server, targets = Loc.both, forward = true) public static void requestItem(Player player, Building build, Item item, int amount){ if(player == null || build == null || !build.interactable(player.team()) || !player.within(build, buildingRange) || player.dead()) return; @@ -1062,12 +1077,16 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ if(build == null){ frag.inv.hide(); frag.config.hideConfig(); + commandBuild = null; return false; } boolean consumed = false, showedInventory = false; - //check if tapped block is configurable - if(build.block.configurable && build.interactable(player.team())){ + //select building for commanding + if(build.block.commandable && commandMode){ + //TODO handled in tap. + consumed = true; + }else if(build.block.configurable && build.interactable(player.team())){ //check if tapped block is configurable consumed = true; if((!frag.config.isShown() && build.shouldShowConfigure(player)) //if the config fragment is hidden, show //alternatively, the current selected block can 'agree' to switch config tiles diff --git a/core/src/mindustry/io/TypeIO.java b/core/src/mindustry/io/TypeIO.java index 8c57f2a34c..1eab0e1fec 100644 --- a/core/src/mindustry/io/TypeIO.java +++ b/core/src/mindustry/io/TypeIO.java @@ -486,6 +486,21 @@ public class TypeIO{ return JsonIO.read(Rules.class, string); } + public static void writeVecNullable(Writes write, @Nullable Vec2 v){ + if(v == null){ + write.f(Float.NaN); + write.f(Float.NaN); + }else{ + write.f(v.x); + write.f(v.y); + } + } + + public static @Nullable Vec2 readVecNullable(Reads read){ + float x = read.f(), y = read.f(); + return Float.isNaN(x) || Float.isNaN(y) ? null : new Vec2(x, y); + } + public static void writeVec2(Writes write, Vec2 v){ if(v == null){ write.f(0); diff --git a/core/src/mindustry/net/Administration.java b/core/src/mindustry/net/Administration.java index 047b297093..adb13a78e2 100644 --- a/core/src/mindustry/net/Administration.java +++ b/core/src/mindustry/net/Administration.java @@ -653,7 +653,7 @@ public class Administration{ } public enum ActionType{ - breakBlock, placeBlock, rotate, configure, withdrawItem, depositItem, control, buildSelect, command, removePlanned, commandUnits + breakBlock, placeBlock, rotate, configure, withdrawItem, depositItem, control, buildSelect, command, removePlanned, commandUnits, commandBuilding } } diff --git a/core/src/mindustry/world/Block.java b/core/src/mindustry/world/Block.java index 53aa9d5e34..924d982271 100644 --- a/core/src/mindustry/world/Block.java +++ b/core/src/mindustry/world/Block.java @@ -177,6 +177,8 @@ public class Block extends UnlockableContent implements Senseable{ public int unitCapModifier = 0; /** Whether the block can be tapped and selected to configure. */ public boolean configurable; + /** If true, this building can be selected like a unit when commanding. */ + public boolean commandable; /** If true, the building inventory can be shown with the config. */ public boolean allowConfigInventory = true; /** If true, this block can be configured by logic. */ diff --git a/core/src/mindustry/world/blocks/units/Reconstructor.java b/core/src/mindustry/world/blocks/units/Reconstructor.java index 0dc3818f7a..121deffb2f 100644 --- a/core/src/mindustry/world/blocks/units/Reconstructor.java +++ b/core/src/mindustry/world/blocks/units/Reconstructor.java @@ -3,6 +3,7 @@ package mindustry.world.blocks.units; import arc.*; import arc.graphics.g2d.*; import arc.math.*; +import arc.math.geom.*; import arc.struct.*; import arc.util.*; import arc.util.io.*; @@ -13,6 +14,7 @@ import mindustry.entities.units.*; import mindustry.game.EventType.*; import mindustry.gen.*; import mindustry.graphics.*; +import mindustry.io.*; import mindustry.logic.*; import mindustry.type.*; import mindustry.ui.*; @@ -31,6 +33,7 @@ public class Reconstructor extends UnitBlock{ super(name); regionRotated1 = 1; regionRotated2 = 2; + commandable = true; } @Override @@ -108,11 +111,22 @@ public class Reconstructor extends UnitBlock{ } public class ReconstructorBuild extends UnitBuild{ + public @Nullable Vec2 commandPos; public float fraction(){ return progress / constructTime; } + @Override + public Vec2 getCommandPosition(){ + return commandPos; + } + + @Override + public void onCommand(Vec2 target){ + commandPos = target; + } + @Override public boolean acceptUnitPayload(Unit unit){ return hasUpgrade(unit.type) && !upgrade(unit.type).isBanned(); @@ -213,6 +227,9 @@ public class Reconstructor extends UnitBlock{ //upgrade the unit if(progress >= constructTime){ payload.unit = upgrade(payload.unit.type).create(payload.unit.team()); + if(commandPos != null && payload.unit.isCommandable()){ + payload.unit.command().commandPosition(commandPos); + } progress %= 1f; Effect.shake(2f, 3f, this); Fx.producesmoke.at(this); @@ -261,7 +278,7 @@ public class Reconstructor extends UnitBlock{ @Override public byte version(){ - return 1; + return 2; } @Override @@ -269,16 +286,20 @@ public class Reconstructor extends UnitBlock{ super.write(write); write.f(progress); + TypeIO.writeVecNullable(write, commandPos); } @Override public void read(Reads read, byte revision){ super.read(read, revision); - if(revision == 1){ + if(revision >= 1){ progress = read.f(); } + if(revision >= 2){ + commandPos = TypeIO.readVecNullable(read); + } } } diff --git a/core/src/mindustry/world/blocks/units/UnitAssembler.java b/core/src/mindustry/world/blocks/units/UnitAssembler.java index b7ebd4263a..ac43eac7e6 100644 --- a/core/src/mindustry/world/blocks/units/UnitAssembler.java +++ b/core/src/mindustry/world/blocks/units/UnitAssembler.java @@ -18,6 +18,7 @@ import mindustry.entities.units.*; import mindustry.game.*; import mindustry.gen.*; import mindustry.graphics.*; +import mindustry.io.*; import mindustry.logic.*; import mindustry.type.*; import mindustry.ui.*; @@ -52,6 +53,7 @@ public class UnitAssembler extends PayloadBlock{ regionRotated1 = 1; sync = true; group = BlockGroup.units; + commandable = true; } public Rect getRect(Rect rect, float x, float y, int rotation){ @@ -190,6 +192,7 @@ public class UnitAssembler extends PayloadBlock{ //holds drone IDs that have been sent, but not synced yet - add to list as soon as possible protected IntSeq whenSyncedUnits = new IntSeq(); + public @Nullable Vec2 commandPos; public Seq units = new Seq<>(); public Seq modules = new Seq<>(); public PayloadSeq blocks = new PayloadSeq(); @@ -398,6 +401,9 @@ public class UnitAssembler extends PayloadBlock{ if(!net.client()){ var unit = plan.unit.create(team); + if(unit != null && unit.isCommandable()){ + unit.command().commandPosition(commandPos); + } unit.set(spawn.x + Mathf.range(0.001f), spawn.y + Mathf.range(0.001f)); unit.rotation = 90f; unit.add(); @@ -546,6 +552,21 @@ public class UnitAssembler extends PayloadBlock{ plan.requirements.contains(b -> b.item == payload.content() && blocks.get(payload.content()) < b.amount); } + @Override + public Vec2 getCommandPosition(){ + return commandPos; + } + + @Override + public void onCommand(Vec2 target){ + commandPos = target; + } + + @Override + public byte version(){ + return 1; + } + @Override public void write(Writes write){ super.write(write); @@ -557,6 +578,7 @@ public class UnitAssembler extends PayloadBlock{ } blocks.write(write); + TypeIO.writeVecNullable(write, commandPos); } @Override @@ -571,6 +593,9 @@ public class UnitAssembler extends PayloadBlock{ whenSyncedUnits.clear(); blocks.read(read); + if(revision >= 1){ + commandPos = TypeIO.readVecNullable(read); + } } } } diff --git a/core/src/mindustry/world/blocks/units/UnitFactory.java b/core/src/mindustry/world/blocks/units/UnitFactory.java index 57d1d95b8b..8653fdfb43 100644 --- a/core/src/mindustry/world/blocks/units/UnitFactory.java +++ b/core/src/mindustry/world/blocks/units/UnitFactory.java @@ -4,6 +4,7 @@ import arc.*; import arc.graphics.*; import arc.graphics.g2d.*; import arc.math.*; +import arc.math.geom.*; import arc.scene.style.*; import arc.scene.ui.layout.*; import arc.struct.*; @@ -15,6 +16,7 @@ import mindustry.entities.units.*; import mindustry.game.EventType.*; import mindustry.gen.*; import mindustry.graphics.*; +import mindustry.io.*; import mindustry.logic.*; import mindustry.type.*; import mindustry.ui.*; @@ -39,6 +41,7 @@ public class UnitFactory extends UnitBlock{ outputsPayload = true; rotate = true; regionRotated1 = 1; + commandable = true; config(Integer.class, (UnitFactoryBuild tile, Integer i) -> { if(!configurable) return; @@ -143,12 +146,23 @@ public class UnitFactory extends UnitBlock{ } public class UnitFactoryBuild extends UnitBuild{ + public @Nullable Vec2 commandPos; public int currentPlan = -1; public float fraction(){ return currentPlan == -1 ? 0 : progress / plans.get(currentPlan).time; } + @Override + public Vec2 getCommandPosition(){ + return commandPos; + } + + @Override + public void onCommand(Vec2 target){ + commandPos = target; + } + @Override public Object senseObject(LAccess sensor){ if(sensor == LAccess.config) return currentPlan == -1 ? null : plans.get(currentPlan).unit; @@ -252,7 +266,11 @@ public class UnitFactory extends UnitBlock{ if(progress >= plan.time && consValid()){ progress %= 1f; - payload = new UnitPayload(plan.unit.create(team)); + Unit unit = plan.unit.create(team); + if(commandPos != null && unit.isCommandable()){ + unit.command().commandPosition(commandPos); + } + payload = new UnitPayload(unit); payVector.setZero(); consume(); Events.fire(new UnitCreateEvent(payload.unit, this)); @@ -287,7 +305,7 @@ public class UnitFactory extends UnitBlock{ @Override public byte version(){ - return 1; + return 2; } @Override @@ -295,6 +313,7 @@ public class UnitFactory extends UnitBlock{ super.write(write); write.f(progress); write.s(currentPlan); + TypeIO.writeVecNullable(write, commandPos); } @Override @@ -302,6 +321,9 @@ public class UnitFactory extends UnitBlock{ super.read(read, revision); progress = read.f(); currentPlan = read.s(); + if(revision >= 2){ + commandPos = TypeIO.readVecNullable(read); + } } } }