Added unit stances
This commit is contained in:
49
core/src/mindustry/ai/UnitStance.java
Normal file
49
core/src/mindustry/ai/UnitStance.java
Normal file
@@ -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<UnitStance> 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;
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,8 @@ public class CommandAI extends AIController{
|
||||
protected Seq<Unit> 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 &&
|
||||
|
||||
@@ -5924,7 +5924,7 @@ public class Blocks{
|
||||
forceDark = true;
|
||||
privileged = true;
|
||||
size = 1;
|
||||
maxInstructionsPerTick = 500;
|
||||
maxInstructionsPerTick = 1000;
|
||||
range = Float.MAX_VALUE;
|
||||
}};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -441,6 +441,9 @@ public class PlacementFragment{
|
||||
UnitCommand[] currentCommand = {null};
|
||||
var commands = new Seq<UnitCommand>();
|
||||
|
||||
UnitStance[] currentStance = {null};
|
||||
var stances = new Seq<UnitStance>();
|
||||
|
||||
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();
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user