Multiple unit stance support

This commit is contained in:
Anuken
2025-07-01 23:23:45 -04:00
parent b6195cc31e
commit 88fc46fed2
8 changed files with 141 additions and 42 deletions

View File

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

View File

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

View File

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

View File

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