diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index c2bf1fe100..346b0cbdea 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -353,6 +353,9 @@ command.rebuild = Rebuild command.assist = Assist Player command.move = Move command.boost = Boost +stance.stop = Cancel Orders +stance.shoot = Stance: Shoot +stance.holdfire = Stance: Hold Fire openlink = Open Link copylink = Copy Link back = Back diff --git a/core/src/mindustry/ai/UnitStance.java b/core/src/mindustry/ai/UnitStance.java new file mode 100644 index 0000000000..c7bbc69241 --- /dev/null +++ b/core/src/mindustry/ai/UnitStance.java @@ -0,0 +1,49 @@ +package mindustry.ai; + +import arc.*; +import arc.scene.style.*; +import arc.struct.*; +import mindustry.gen.*; + +public class UnitStance{ + /** List of all stances by ID. */ + public static final Seq all = new Seq<>(); + + public static final UnitStance + + stopStance = new UnitStance("stop", "cancel"), //not a real stance, cannot be selected, just cancels ordewrs + shootStance = new UnitStance("shoot", "commandAttack"), + holdFireStance = new UnitStance("holdfire", "none"); + + /** Unique ID number. */ + public final int id; + /** Named used for tooltip/description. */ + public final String name; + /** Name of UI icon (from Icon class). */ + public final String icon; + + public UnitStance(String name, String icon){ + this.name = name; + this.icon = icon; + + id = all.size; + all.add(this); + } + + public String localized(){ + return Core.bundle.get("stance." + name); + } + + public TextureRegionDrawable getIcon(){ + return Icon.icons.get(icon, Icon.cancel); + } + + public char getEmoji() { + return (char) Iconc.codes.get(icon, Iconc.cancel); + } + + @Override + public String toString(){ + return "UnitStance:" + name; + } +} diff --git a/core/src/mindustry/ai/types/CommandAI.java b/core/src/mindustry/ai/types/CommandAI.java index 4434f2f8a9..bb012445cf 100644 --- a/core/src/mindustry/ai/types/CommandAI.java +++ b/core/src/mindustry/ai/types/CommandAI.java @@ -30,6 +30,8 @@ public class CommandAI extends AIController{ protected Seq local = new Seq<>(false); protected boolean flocked; + /** Stance, usually related to firing mode. */ + public UnitStance stance = UnitStance.shootStance; /** Current command this unit is following. */ public @Nullable UnitCommand command; /** Current controller instance based on command. */ @@ -62,6 +64,8 @@ public class CommandAI extends AIController{ @Override public void updateUnit(){ + //this should not be possible + if(stance == UnitStance.stopStance) stance = UnitStance.shootStance; //remove invalid targets if(commandQueue.any()){ @@ -91,6 +95,12 @@ public class CommandAI extends AIController{ } } + public void clearCommands(){ + commandQueue.clear(); + targetPos = null; + attackTarget = null; + } + public void defaultBehavior(){ //acquiring naval targets isn't supported yet, so use the fallback dumb AI @@ -256,6 +266,11 @@ public class CommandAI extends AIController{ } } + @Override + public boolean shouldFire(){ + return stance != UnitStance.holdFireStance; + } + @Override public void hit(Bullet bullet){ if(unit.team.isAI() && bullet.owner instanceof Teamc teamc && teamc.team() != unit.team && attackTarget == null && diff --git a/core/src/mindustry/content/Blocks.java b/core/src/mindustry/content/Blocks.java index 6192d9b7cd..f7b63bc6a4 100644 --- a/core/src/mindustry/content/Blocks.java +++ b/core/src/mindustry/content/Blocks.java @@ -5924,7 +5924,7 @@ public class Blocks{ forceDark = true; privileged = true; size = 1; - maxInstructionsPerTick = 500; + maxInstructionsPerTick = 1000; range = Float.MAX_VALUE; }}; diff --git a/core/src/mindustry/entities/units/AIController.java b/core/src/mindustry/entities/units/AIController.java index 6aef11e104..547775896a 100644 --- a/core/src/mindustry/entities/units/AIController.java +++ b/core/src/mindustry/entities/units/AIController.java @@ -182,7 +182,12 @@ public class AIController implements UnitController{ mount.aimY = to.y; } - unit.isShooting |= (mount.shoot = mount.rotate = shoot); + mount.shoot = mount.rotate = shoot; + if(!shouldFire()){ + mount.shoot = false; + } + + unit.isShooting |= mount.shoot; if(mount.target == null && !shoot && !Angles.within(mount.rotation, mount.weapon.baseRotation, 0.01f) && noTargetTime >= rotateBackTimer){ mount.rotate = true; @@ -202,6 +207,11 @@ public class AIController implements UnitController{ return Units.invalidateTarget(target, unit.team, x, y, range); } + /** @return whether the unit should actually fire bullets (as opposed to just targeting something) */ + public boolean shouldFire(){ + return true; + } + public boolean shouldShoot(){ return true; } diff --git a/core/src/mindustry/input/DesktopInput.java b/core/src/mindustry/input/DesktopInput.java index 94385e0148..760c8a0dcd 100644 --- a/core/src/mindustry/input/DesktopInput.java +++ b/core/src/mindustry/input/DesktopInput.java @@ -473,7 +473,7 @@ public class DesktopInput extends InputHandler{ cursorType = ui.targetCursor; } - if(input.keyTap(Binding.command_queue) && keybinds.get(Binding.command_mode).key.type != KeyType.mouse){ + if(input.keyTap(Binding.command_queue) && keybinds.get(Binding.command_queue).key.type != KeyType.mouse){ commandTap(input.mouseX(), input.mouseY(), true); } } diff --git a/core/src/mindustry/input/InputHandler.java b/core/src/mindustry/input/InputHandler.java index 0ecc38fe73..081ec89382 100644 --- a/core/src/mindustry/input/InputHandler.java +++ b/core/src/mindustry/input/InputHandler.java @@ -312,6 +312,29 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ } } + @Remote(called = Loc.server, targets = Loc.both, forward = true) + public static void setUnitStance(Player player, int[] unitIds, UnitStance stance){ + if(player == null || unitIds == null || stance == null) return; + + if(net.server() && !netServer.admins.allowAction(player, ActionType.commandUnits, event -> { + event.unitIDs = unitIds; + })){ + throw new ValidateException(player, "Player cannot command units."); + } + + for(int id : unitIds){ + Unit unit = Groups.unit.getByID(id); + if(unit != null && unit.team == player.team() && unit.controller() instanceof CommandAI ai){ + if(stance == UnitStance.stopStance){ //not a real stance, just cancels orders + ai.clearCommands(); + }else{ + ai.stance = stance; + } + unit.lastCommanded = player.coloredName(); + } + } + } + @Remote(called = Loc.server, targets = Loc.both, forward = true) public static void commandBuilding(Player player, int[] buildings, Vec2 target){ if(player == null || target == null) return; diff --git a/core/src/mindustry/io/TypeIO.java b/core/src/mindustry/io/TypeIO.java index 48ab654b15..491c3548bf 100644 --- a/core/src/mindustry/io/TypeIO.java +++ b/core/src/mindustry/io/TypeIO.java @@ -314,6 +314,16 @@ public class TypeIO{ return val == 255 ? null : UnitCommand.all.get(val); } + public static void writeStance(Writes write, @Nullable UnitStance stance){ + write.b(stance == null ? 255 : stance.id); + } + + public static UnitStance readStance(Reads read){ + int val = read.ub(); + //never returns null + return val == 255 ? UnitStance.shootStance : UnitStance.all.get(val); + } + public static void writeEntity(Writes write, Entityc entity){ write.i(entity == null ? -1 : entity.id()); } @@ -472,7 +482,7 @@ public class TypeIO{ write.b(3); write.i(logic.controller.pos()); }else if(control instanceof CommandAI ai){ - write.b(7); + write.b(8); write.bool(ai.attackTarget != null); write.bool(ai.targetPos != null); @@ -507,6 +517,8 @@ public class TypeIO{ write.b(3); } } + + writeStance(write, ai.stance); }else if(control instanceof AssemblerAI){ //hate write.b(5); }else{ @@ -538,8 +550,8 @@ public class TypeIO{ out.controller = world.build(pos); return out; } - //type 4 is the old CommandAI with no commandIndex, type 6 is the new one with the index as a single byte, type 7 is the one with the command queue - }else if(type == 4 || type == 6 || type == 7){ + //type 4 is the old CommandAI with no commandIndex, type 6 is the new one with the index as a single byte, type 7 is the one with the command queue, 8 adds a stance + }else if(type == 4 || type == 6 || type == 7 || type == 8){ CommandAI ai = prev instanceof CommandAI pai ? pai : new CommandAI(); boolean hasAttack = read.bool(), hasPos = read.bool(); @@ -562,13 +574,13 @@ public class TypeIO{ ai.attackTarget = null; } - if(type == 6 || type == 7){ + if(type == 6 || type == 7 || type == 8){ byte id = read.b(); ai.command = id < 0 ? null : UnitCommand.all.get(id); } //command queue only in type 7 - if(type == 7){ + if(type == 7 || type == 8){ ai.commandQueue.clear(); int length = read.ub(); for(int i = 0; i < length; i++){ @@ -590,6 +602,10 @@ public class TypeIO{ } } + if(type == 8){ + ai.stance = readStance(read); + } + return ai; }else if(type == 5){ //augh diff --git a/core/src/mindustry/type/UnitType.java b/core/src/mindustry/type/UnitType.java index c44769f4d5..9a486773c5 100644 --- a/core/src/mindustry/type/UnitType.java +++ b/core/src/mindustry/type/UnitType.java @@ -296,6 +296,8 @@ public class UnitType extends UnlockableContent implements Senseable{ public UnitCommand[] commands = {}; /** Command to assign to this unit upon creation. Null indicates the first command in the array. */ public @Nullable UnitCommand defaultCommand; + /** Stances this unit can have. An empty array means stances will be assigned based on unit capabilities in init(). */ + public UnitStance[] stances = {}; /** color for outline generated around sprites */ public Color outlineColor = Pal.darkerMetal; @@ -696,7 +698,9 @@ public class UnitType extends UnlockableContent implements Senseable{ } //if a status effects slows a unit when firing, don't shoot while moving. - autoFindTarget = !weapons.contains(w -> w.shootStatus.speedMultiplier < 0.99f) || alwaysShootWhenMoving; + if(autoFindTarget){ + autoFindTarget = !weapons.contains(w -> w.shootStatus.speedMultiplier < 0.99f) || alwaysShootWhenMoving; + } clipSize = Math.max(clipSize, lightRadius * 1.1f); singleTarget = weapons.size <= 1 && !forceMultiTarget; @@ -830,6 +834,14 @@ public class UnitType extends UnlockableContent implements Senseable{ commands = cmds.toArray(); } + if(stances.length == 0){ + if(canAttack){ + stances = new UnitStance[]{UnitStance.stopStance, UnitStance.shootStance, UnitStance.holdFireStance}; + }else{ + stances = new UnitStance[]{UnitStance.stopStance, UnitStance.shootStance}; + } + } + //dynamically create ammo capacity based on firing rate if(ammoCapacity < 0){ float shotsPerSecond = weapons.sumf(w -> w.useAmmo ? 60f / w.reload : 0f); diff --git a/core/src/mindustry/ui/fragments/PlacementFragment.java b/core/src/mindustry/ui/fragments/PlacementFragment.java index 7c0ed867b6..b3390df5e5 100644 --- a/core/src/mindustry/ui/fragments/PlacementFragment.java +++ b/core/src/mindustry/ui/fragments/PlacementFragment.java @@ -441,6 +441,9 @@ public class PlacementFragment{ UnitCommand[] currentCommand = {null}; var commands = new Seq(); + UnitStance[] currentStance = {null}; + var stances = new Seq(); + rebuildCommand = () -> { u.clearChildren(); var units = control.input.selectedUnits; @@ -450,7 +453,8 @@ public class PlacementFragment{ counts[unit.type.id] ++; } commands.clear(); - boolean firstCommand = false; + stances.clear(); + boolean firstCommand = false, firstStance = false; Table unitlist = u.table().growX().left().get(); unitlist.left(); @@ -489,13 +493,23 @@ public class PlacementFragment{ //remove commands that this next unit type doesn't have commands.removeAll(com -> !Structs.contains(type.commands, com)); } + + if(!firstStance){ + stances.add(type.stances); + firstStance = true; + }else{ + //remove commands that this next unit type doesn't have + stances.removeAll(st -> !Structs.contains(type.stances, st)); + } } } + //list commands if(commands.size > 1){ u.row(); u.table(coms -> { + coms.left(); for(var command : commands){ coms.button(Icon.icons.get(command.icon, Icon.cancel), Styles.clearNoneTogglei, () -> { IntSeq ids = new IntSeq(); @@ -508,37 +522,71 @@ public class PlacementFragment{ } }).fillX().padTop(4f).left(); } + + //list stances + if(stances.size > 0){ + u.row(); + + u.table(coms -> { + coms.left(); + for(var stance : stances){ + coms.button(Icon.icons.get(stance.icon, Icon.cancel), Styles.clearNoneTogglei, () -> { + IntSeq ids = new IntSeq(); + for(var unit : units){ + ids.add(unit.id); + } + + Call.setUnitStance(Vars.player, ids.toArray(), stance); + }).checked(i -> currentStance[0] == stance).size(50f).tooltip(stance.localized()); + } + }).fillX().padTop(4f).left(); + } }else{ u.add(Core.bundle.get("commandmode.nounits")).color(Color.lightGray).growX().center().labelAlign(Align.center).pad(6); } }; u.update(() -> { - boolean hadCommand = false; - UnitCommand shareCommand = null; + { + boolean hadCommand = false, hadStance = false; + UnitCommand shareCommand = null; + UnitStance shareStance = null; - //find the command that all units have, or null if they do not share one - for(var unit : control.input.selectedUnits){ - if(unit.isCommandable()){ - var nextCommand = unit.command().command; + //find the command that all units have, or null if they do not share one + for(var unit : control.input.selectedUnits){ + if(unit.isCommandable()){ + var nextCommand = unit.command().command; - if(hadCommand){ - if(shareCommand != nextCommand){ - shareCommand = null; + if(hadCommand){ + if(shareCommand != nextCommand){ + shareCommand = null; + } + }else{ + shareCommand = nextCommand; + hadCommand = true; + } + + var nextStance = unit.command().stance; + + if(hadStance){ + if(shareStance != nextStance){ + shareStance = null; + } + }else{ + shareStance = nextStance; + hadStance = true; } - }else{ - shareCommand = nextCommand; - hadCommand = true; } } - } - currentCommand[0] = shareCommand; + currentCommand[0] = shareCommand; + currentStance[0] = shareStance; - int size = control.input.selectedUnits.size; - if(curCount[0] != size){ - curCount[0] = size; - rebuildCommand.run(); + int size = control.input.selectedUnits.size; + if(curCount[0] != size){ + curCount[0] = size; + rebuildCommand.run(); + } } }); rebuildCommand.run(); diff --git a/core/src/mindustry/world/blocks/storage/CoreBlock.java b/core/src/mindustry/world/blocks/storage/CoreBlock.java index a719992935..ff24181b79 100644 --- a/core/src/mindustry/world/blocks/storage/CoreBlock.java +++ b/core/src/mindustry/world/blocks/storage/CoreBlock.java @@ -361,11 +361,11 @@ public class CoreBlock extends StorageBlock{ public void changeTeam(Team next){ if(this.team == next) return; - state.teams.unregisterCore(this); + onRemoved(); super.changeTeam(next); - state.teams.registerCore(this); + onProximityUpdate(); Events.fire(new CoreChangeEvent(this)); }