Added unit stances

This commit is contained in:
Anuken
2023-09-20 21:55:06 -04:00
parent 0d1c56fb60
commit 3e15f70efa
11 changed files with 206 additions and 30 deletions

View 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;
}
}

View File

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

View File

@@ -5924,7 +5924,7 @@ public class Blocks{
forceDark = true;
privileged = true;
size = 1;
maxInstructionsPerTick = 500;
maxInstructionsPerTick = 1000;
range = Float.MAX_VALUE;
}};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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