Multiple unit stance support
This commit is contained in:
@@ -8,17 +8,26 @@ import mindustry.type.*;
|
||||
|
||||
public class ItemUnitStance extends UnitStance{
|
||||
private static ObjectMap<Item, ItemUnitStance> itemToStance = new ObjectMap<>();
|
||||
private static Seq<ItemUnitStance> all = new Seq<>();
|
||||
|
||||
public final Item item;
|
||||
|
||||
public ItemUnitStance(Item item){
|
||||
super("item-" + item.name, "item-" + item.name, null);
|
||||
this.item = item;
|
||||
|
||||
incompatibleStances.add(UnitStance.mineAuto).addAll(UnitStance.mineAuto.incompatibleStances);
|
||||
|
||||
itemToStance.put(item, this);
|
||||
all.add(this);
|
||||
}
|
||||
|
||||
public static @Nullable ItemUnitStance getByItem(Item item){
|
||||
return itemToStance.get(item);
|
||||
return item == null ? null : itemToStance.get(item);
|
||||
}
|
||||
|
||||
public static Seq<ItemUnitStance> all(){
|
||||
return all;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -3,6 +3,7 @@ package mindustry.ai;
|
||||
import arc.*;
|
||||
import arc.input.*;
|
||||
import arc.scene.style.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.*;
|
||||
import mindustry.ctype.*;
|
||||
@@ -17,11 +18,33 @@ public class UnitStance extends MappableContent{
|
||||
public String icon;
|
||||
/** Key to press for this stance. */
|
||||
public @Nullable KeyBind keybind;
|
||||
/** Stances that are mutually exclusive to this stance. This is used for convenience, for writing only! */
|
||||
public Seq<UnitStance> incompatibleStances = new Seq<>();
|
||||
/** Incompatible stances as a bitset for easier operations. This is where incompatibility is actually stored. */
|
||||
public Bits incompatibleBits = new Bits(1);
|
||||
/** If true, this stance can be toggled on or off. */
|
||||
public boolean toggle = true;
|
||||
|
||||
public UnitStance(String name, String icon, KeyBind keybind){
|
||||
public UnitStance(String name, String icon, KeyBind keybind, boolean toggle){
|
||||
super(name);
|
||||
this.icon = icon;
|
||||
this.keybind = keybind;
|
||||
this.toggle = toggle;
|
||||
}
|
||||
|
||||
public UnitStance(String name, String icon, KeyBind keybind){
|
||||
this(name, icon, keybind, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(){
|
||||
super.init();
|
||||
|
||||
for(var stance : incompatibleStances){
|
||||
if(stance == this) continue;
|
||||
incompatibleBits.set(stance.id);
|
||||
stance.incompatibleBits.set(id);
|
||||
}
|
||||
}
|
||||
|
||||
public String localized(){
|
||||
@@ -47,13 +70,15 @@ public class UnitStance extends MappableContent{
|
||||
}
|
||||
|
||||
public static void loadAll(){
|
||||
stop = new UnitStance("stop", "cancel", Binding.cancelOrders);
|
||||
shoot = new UnitStance("shoot", "commandAttack", Binding.unitStanceShoot);
|
||||
holdFire = new UnitStance("holdfire", "none", Binding.unitStanceHoldFire);
|
||||
stop = new UnitStance("stop", "cancel", Binding.cancelOrders, false);
|
||||
shoot = new UnitStance("shoot", "commandAttack", Binding.unitStanceShoot, false);
|
||||
holdFire = new UnitStance("holdfire", "none", Binding.unitStanceHoldFire, false);
|
||||
pursueTarget = new UnitStance("pursuetarget", "right", Binding.unitStancePursueTarget);
|
||||
patrol = new UnitStance("patrol", "refresh", Binding.unitStancePatrol);
|
||||
ram = new UnitStance("ram", "rightOpen", Binding.unitStanceRam);
|
||||
mineAuto = new UnitStance("mineauto", "settings", null);
|
||||
mineAuto = new UnitStance("mineauto", "settings", null, false);
|
||||
|
||||
shoot.incompatibleStances.add(holdFire);
|
||||
|
||||
//Only vanilla items are supported for now
|
||||
for(Item item : Vars.content.items()){
|
||||
|
||||
@@ -40,15 +40,20 @@ public class CommandAI extends AIController{
|
||||
protected float payloadPickupCooldown;
|
||||
protected int transferState = transferStateNone;
|
||||
|
||||
/** Stance, usually related to firing mode. */
|
||||
public UnitStance stance = UnitStance.shoot;
|
||||
/** Current command this unit is following. */
|
||||
public UnitCommand command;
|
||||
/** Stance, usually related to firing mode. Each bit is a stance ID. */
|
||||
public Bits stances = new Bits(content.unitStances().size);
|
||||
/** Current controller instance based on command. */
|
||||
protected @Nullable AIController commandController;
|
||||
/** Last command type assigned. Used for detecting command changes. */
|
||||
protected @Nullable UnitCommand lastCommand;
|
||||
|
||||
{
|
||||
//TODO: is this necessary when 'hold fire' can be a toggle?
|
||||
setStance(UnitStance.shoot);
|
||||
}
|
||||
|
||||
public UnitCommand currentCommand(){
|
||||
return command == null ? UnitCommand.moveCommand : command;
|
||||
}
|
||||
@@ -63,6 +68,35 @@ public class CommandAI extends AIController{
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasStance(@Nullable UnitStance stance){
|
||||
return stance != null && stances.get(stance.id);
|
||||
}
|
||||
|
||||
public void setStance(UnitStance stance, boolean enabled){
|
||||
if(enabled){
|
||||
setStance(stance);
|
||||
}else{
|
||||
disableStance(stance);
|
||||
}
|
||||
}
|
||||
|
||||
public void setStance(UnitStance stance){
|
||||
stances.andNot(stance.incompatibleBits);
|
||||
stances.set(stance.id);
|
||||
stanceChanged();
|
||||
}
|
||||
|
||||
public void disableStance(UnitStance stance){
|
||||
stances.clear(stance.id);
|
||||
stanceChanged();
|
||||
}
|
||||
|
||||
public void stanceChanged(){
|
||||
if(commandController != null && !(commandController instanceof CommandAI)){
|
||||
commandController.stanceChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(){
|
||||
if(command == null){
|
||||
@@ -82,21 +116,18 @@ public class CommandAI extends AIController{
|
||||
|
||||
@Override
|
||||
public void updateUnit(){
|
||||
//this should not be possible
|
||||
if(stance == UnitStance.stop) stance = UnitStance.shoot;
|
||||
|
||||
//fix incorrect stance when mining
|
||||
if(command == UnitCommand.mineCommand && stance != UnitStance.mineAuto && !(stance instanceof ItemUnitStance)){
|
||||
stance = UnitStance.mineAuto;
|
||||
if(command == UnitCommand.mineCommand && !hasStance(UnitStance.mineAuto) && !ItemUnitStance.all().contains(this::hasStance)){
|
||||
setStance(UnitStance.mineAuto);
|
||||
}
|
||||
|
||||
//pursue the target if relevant
|
||||
if(stance == UnitStance.pursueTarget && target != null && attackTarget == null && targetPos == null){
|
||||
if(hasStance(UnitStance.pursueTarget) && !hasStance(UnitStance.patrol) && target != null && attackTarget == null && targetPos == null){
|
||||
commandTarget(target, false);
|
||||
}
|
||||
|
||||
//pursue the target for patrol, keeping the current position
|
||||
if(stance == UnitStance.patrol && target != null && attackTarget == null){
|
||||
if(hasStance(UnitStance.patrol) && hasStance(UnitStance.pursueTarget) && target != null && attackTarget == null){
|
||||
//commanding a target overwrites targetPos, so add it to the queue
|
||||
if(targetPos != null){
|
||||
commandQueue.add(targetPos.cpy());
|
||||
@@ -207,6 +238,8 @@ public class CommandAI extends AIController{
|
||||
finishPath();
|
||||
}
|
||||
|
||||
boolean ramming = hasStance(UnitStance.ram);
|
||||
|
||||
if(attackTarget != null){
|
||||
if(targetPos == null){
|
||||
targetPos = new Vec2();
|
||||
@@ -214,7 +247,7 @@ public class CommandAI extends AIController{
|
||||
}
|
||||
targetPos.set(attackTarget);
|
||||
|
||||
if(unit.isGrounded() && attackTarget instanceof Building build && build.tile.solid() && unit.type.pathCostId != ControlPathfinder.costIdLegs && stance != UnitStance.ram){
|
||||
if(unit.isGrounded() && attackTarget instanceof Building build && build.tile.solid() && unit.type.pathCostId != ControlPathfinder.costIdLegs && !ramming){
|
||||
Tile best = build.findClosestEdge(unit, Tile::solid);
|
||||
if(best != null){
|
||||
targetPos.set(best);
|
||||
@@ -225,7 +258,7 @@ public class CommandAI extends AIController{
|
||||
boolean alwaysArrive = false;
|
||||
|
||||
float engageRange = unit.type.range - 10f;
|
||||
boolean withinAttackRange = attackTarget != null && unit.within(attackTarget, engageRange) && stance != UnitStance.ram;
|
||||
boolean withinAttackRange = attackTarget != null && unit.within(attackTarget, engageRange) && !ramming;
|
||||
|
||||
if(targetPos != null){
|
||||
boolean move = true, isFinalPoint = commandQueue.size == 0;
|
||||
@@ -239,16 +272,17 @@ public class CommandAI extends AIController{
|
||||
|
||||
Building targetBuild = world.buildWorld(targetPos.x, targetPos.y);
|
||||
|
||||
|
||||
//TODO: should the unit stop when it finds a target?
|
||||
if(
|
||||
(stance == UnitStance.patrol && target != null && unit.within(target, unit.type.range - 2f) && !unit.type.circleTarget) ||
|
||||
(hasStance(UnitStance.patrol) && !hasStance(UnitStance.pursueTarget) && target != null && unit.within(target, unit.type.range - 2f) && !unit.type.circleTarget) ||
|
||||
(command == UnitCommand.enterPayloadCommand && unit.within(targetPos, 4f) || (targetBuild != null && unit.within(targetBuild, targetBuild.block.size * tilesize/2f * 0.9f))) ||
|
||||
(command == UnitCommand.loopPayloadCommand && unit.within(targetPos, 10f))
|
||||
){
|
||||
move = false;
|
||||
}
|
||||
|
||||
if(unit.isGrounded() && stance != UnitStance.ram){
|
||||
if(unit.isGrounded() && !ramming){
|
||||
//TODO: blocking enable or disable?
|
||||
if(timer.get(timerTarget3, avoidInterval)){
|
||||
Vec2 dstPos = Tmp.v1.trns(unit.rotation, unit.hitSize/2f);
|
||||
@@ -284,7 +318,7 @@ public class CommandAI extends AIController{
|
||||
noFound[0] = false;
|
||||
vecOut.set(vecMovePos);
|
||||
}else{
|
||||
move = controlPath.getPathPosition(unit, vecMovePos, targetPos, vecOut, noFound) && (!blockingUnit || timeSpentBlocked > maxBlockTime);
|
||||
move &= controlPath.getPathPosition(unit, vecMovePos, targetPos, vecOut, noFound) && (!blockingUnit || timeSpentBlocked > maxBlockTime);
|
||||
|
||||
//TODO: what to do when there's a target and it can't be reached?
|
||||
/*
|
||||
@@ -320,7 +354,7 @@ public class CommandAI extends AIController{
|
||||
moveTo(vecOut,
|
||||
withinAttackRange ? engageRange :
|
||||
unit.isGrounded() ? 0f :
|
||||
attackTarget != null && stance != UnitStance.ram ? engageRange : 0f,
|
||||
attackTarget != null && !ramming ? engageRange : 0f,
|
||||
unit.isFlying() ? 40f : 100f, false, null, isFinalPoint || alwaysArrive);
|
||||
}
|
||||
}
|
||||
@@ -412,7 +446,7 @@ public class CommandAI extends AIController{
|
||||
commandPosition(position);
|
||||
}
|
||||
|
||||
if(prev != null && (stance == UnitStance.patrol || command == UnitCommand.loopPayloadCommand)){
|
||||
if(prev != null && (hasStance(UnitStance.patrol) || command == UnitCommand.loopPayloadCommand)){
|
||||
commandQueue.add(prev.cpy());
|
||||
}
|
||||
|
||||
@@ -454,7 +488,7 @@ public class CommandAI extends AIController{
|
||||
|
||||
@Override
|
||||
public boolean shouldFire(){
|
||||
return stance != UnitStance.holdFire;
|
||||
return !hasStance(UnitStance.holdFire);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -13,6 +13,14 @@ public class MinerAI extends AIController{
|
||||
public Item targetItem;
|
||||
public Tile ore;
|
||||
|
||||
@Override
|
||||
public void stanceChanged(){
|
||||
if(targetItem != null && unit.controller() instanceof CommandAI ai && !ai.hasStance(UnitStance.mineAuto) && !ai.hasStance(ItemUnitStance.getByItem(targetItem))){
|
||||
mining = false;
|
||||
targetItem = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMovement(){
|
||||
Building core = unit.closestCore();
|
||||
@@ -23,13 +31,15 @@ public class MinerAI extends AIController{
|
||||
unit.mineTile(null);
|
||||
}
|
||||
|
||||
Item autoItem = unit.controller() instanceof CommandAI ai && ai.stance instanceof ItemUnitStance stance ? stance.item : null;
|
||||
CommandAI ai = unit.controller() instanceof CommandAI a ? a : null;
|
||||
|
||||
if(mining){
|
||||
if(autoItem != null){
|
||||
targetItem = autoItem;
|
||||
}else if(timer.get(timerTarget2, 60 * 4) || targetItem == null){
|
||||
targetItem = unit.type.mineItems.min(i -> indexer.hasOre(i) && unit.canMine(i), i -> core.items.get(i));
|
||||
if(timer.get(timerTarget2, 60 * 4) || targetItem == null){
|
||||
if(ai != null && !ai.hasStance(UnitStance.mineAuto)){
|
||||
targetItem = content.items().min(i -> indexer.hasOre(i) && unit.canMine(i) && ai.hasStance(ItemUnitStance.getByItem(i)), i -> core.items.get(i));
|
||||
}else{
|
||||
targetItem = unit.type.mineItems.min(i -> indexer.hasOre(i) && unit.canMine(i), i -> core.items.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
//core full of the target item, do nothing
|
||||
|
||||
@@ -51,6 +51,9 @@ public class AIController implements UnitController{
|
||||
updateMovement();
|
||||
}
|
||||
|
||||
/** Called when the parent CommandAI changes its stance. */
|
||||
public void stanceChanged(){}
|
||||
|
||||
/**
|
||||
* @return whether controller state should not be reset after reading.
|
||||
* Do not override unless you know exactly what you are doing.
|
||||
|
||||
@@ -372,15 +372,18 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
//make sure its current stance is valid with its current command
|
||||
stancesOut.clear();
|
||||
unit.type.getUnitStances(unit, stancesOut);
|
||||
if(stancesOut.size > 0 && !stancesOut.contains(ai.stance)){
|
||||
ai.stance = stancesOut.first();
|
||||
for(var stance : content.unitStances()){
|
||||
//disable stances that the unit does not support anymore (TODO: this is slow!)
|
||||
if(ai.hasStance(stance) && !stancesOut.contains(stance)){
|
||||
ai.disableStance(stance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(called = Loc.server, targets = Loc.both, forward = true)
|
||||
public static void setUnitStance(Player player, int[] unitIds, UnitStance stance){
|
||||
public static void setUnitStance(Player player, int[] unitIds, UnitStance stance, boolean enable){
|
||||
if(player == null || unitIds == null || stance == null) return;
|
||||
|
||||
if(net.server() && !netServer.admins.allowAction(player, ActionType.commandUnits, event -> {
|
||||
@@ -395,7 +398,8 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
if(stance == UnitStance.stop){ //not a real stance, just cancels orders
|
||||
ai.clearCommands();
|
||||
}else if(unit.type.allowStance(unit, stance)){
|
||||
ai.stance = stance;
|
||||
//if toggle is not allowed, the stance will always be set to true when pressed
|
||||
ai.setStance(stance, !stance.toggle || enable);
|
||||
}
|
||||
unit.lastCommanded = player.coloredName();
|
||||
}
|
||||
|
||||
@@ -523,7 +523,7 @@ public class TypeIO{
|
||||
write.b(3);
|
||||
write.i(logic.controller.pos());
|
||||
}else if(control instanceof CommandAI ai){
|
||||
write.b(8);
|
||||
write.b(9);
|
||||
write.bool(ai.attackTarget != null);
|
||||
write.bool(ai.targetPos != null);
|
||||
|
||||
@@ -559,7 +559,16 @@ public class TypeIO{
|
||||
}
|
||||
}
|
||||
|
||||
writeStance(write, ai.stance);
|
||||
int count = content.unitStances().count(ai::hasStance);
|
||||
|
||||
write.b(count);
|
||||
|
||||
for(var stance : content.unitStances()){
|
||||
if(ai.hasStance(stance)){
|
||||
writeStance(write, stance);
|
||||
}
|
||||
}
|
||||
|
||||
}else if(control instanceof AssemblerAI){ //hate
|
||||
write.b(5);
|
||||
}else{
|
||||
@@ -591,8 +600,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, 8 adds a stance
|
||||
}else if(type == 4 || type == 6 || type == 7 || type == 8){
|
||||
//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, 9 adds multiple stances
|
||||
}else if(type == 4 || type == 6 || type == 7 || type == 8 || type == 9){
|
||||
CommandAI ai = prev instanceof CommandAI pai ? pai : new CommandAI();
|
||||
|
||||
boolean hasAttack = read.bool(), hasPos = read.bool();
|
||||
@@ -616,14 +625,14 @@ public class TypeIO{
|
||||
ai.attackTarget = null;
|
||||
}
|
||||
|
||||
if(type == 6 || type == 7 || type == 8){
|
||||
if(type == 6 || type == 7 || type == 8 || type == 9){
|
||||
byte id = read.b();
|
||||
ai.command = id < 0 ? null : content.unitCommand(id);
|
||||
if(ai.command == null) ai.command = UnitCommand.moveCommand;
|
||||
}
|
||||
|
||||
//command queue only in type 7/8
|
||||
if(type == 7 || type == 8){
|
||||
if(type == 7 || type == 8 || type == 9){
|
||||
ai.commandQueue.clear();
|
||||
int length = read.ub();
|
||||
for(int i = 0; i < length; i++){
|
||||
@@ -646,7 +655,12 @@ public class TypeIO{
|
||||
}
|
||||
|
||||
if(type == 8){
|
||||
ai.stance = readStance(read);
|
||||
ai.setStance(readStance(read));
|
||||
}else if(type == 9){
|
||||
int stances = read.ub();
|
||||
for(int i = 0; i < stances; i++){
|
||||
ai.setStance(readStance(read));
|
||||
}
|
||||
}
|
||||
|
||||
return ai;
|
||||
|
||||
@@ -581,7 +581,7 @@ public class PlacementFragment{
|
||||
for(var stance : stances){
|
||||
|
||||
coms.button(stance.getIcon(), Styles.clearNoneTogglei, () -> {
|
||||
Call.setUnitStance(player, units.mapInt(un -> un.id, un -> un.type.allowStance(un, stance)).toArray(), stance);
|
||||
Call.setUnitStance(player, units.mapInt(un -> un.id, un -> un.type.allowStance(un, stance)).toArray(), stance, !activeStances.get(stance.id));
|
||||
}).checked(i -> activeStances.get(stance.id)).size(50f).tooltip(stance.localized(), true);
|
||||
|
||||
if(++scol % 6 == 0) coms.row();
|
||||
@@ -604,7 +604,7 @@ public class PlacementFragment{
|
||||
for(var unit : control.input.selectedUnits){
|
||||
if(unit.controller() instanceof CommandAI cmd){
|
||||
activeCommands.set(cmd.command.id);
|
||||
activeStances.set(cmd.stance.id);
|
||||
activeStances.set(cmd.stances);
|
||||
}
|
||||
|
||||
stancesOut.clear();
|
||||
@@ -631,7 +631,7 @@ public class PlacementFragment{
|
||||
for(UnitStance stance : stances){
|
||||
//first stance must always be the stop stance
|
||||
if(stance.keybind != null && Core.input.keyTap(stance.keybind)){
|
||||
Call.setUnitStance(player, control.input.selectedUnits.mapInt(un -> un.id, un -> un.type.allowStance(un, stance)).toArray(), stance);
|
||||
Call.setUnitStance(player, control.input.selectedUnits.mapInt(un -> un.id, un -> un.type.allowStance(un, stance)).toArray(), stance, !activeStances.get(stance.id));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user