Added 'hold position' stance (closes Anuken/Mindustry-Suggestions/issues/6157)

This commit is contained in:
Anuken
2026-03-08 22:00:02 -04:00
parent 45e5ca3612
commit 19ab8423eb
10 changed files with 92 additions and 31 deletions

View File

@@ -380,6 +380,7 @@ stance.shoot = Stance: Shoot
stance.holdfire = Stance: Hold Fire
stance.pursuetarget = Stance: Pursue Target
stance.patrol = Stance: Patrol Path
stance.holdposition = Stance: Hold Position
stance.ram = Stance: Ram\n[lightgray]Straight line movement, no pathfinding
stance.boost = Boost
stance.mineauto = Automatic Mining
@@ -1364,6 +1365,7 @@ keybind.unit_stance_pursue_target.name = Unit Stance: Pursue Target
keybind.unit_stance_patrol.name = Unit Stance: Patrol
keybind.unit_stance_ram.name = Unit Stance: Ram
keybind.unit_stance_boost.name = Unit Stance: Boost
keybind.unit_stance_hold_position.name = Unit Stance: Hold Position
keybind.unit_command_move.name = Unit Command: Move
keybind.unit_command_repair.name = Unit Command: Repair

View File

@@ -4,6 +4,7 @@ import arc.*;
import arc.func.*;
import arc.input.*;
import arc.scene.style.*;
import arc.struct.*;
import arc.util.*;
import mindustry.ai.types.*;
import mindustry.ctype.*;
@@ -27,12 +28,14 @@ public class UnitCommand extends MappableContent{
public boolean resetTarget = true;
/** Whether to snap the command destination to ally buildings. */
public boolean snapToBuilding = false;
/** */
/** If true, the unit will arrive at this command's exact endpoint. */
public boolean exactArrival = false;
/** If true, this command refreshes the list of stances when selected TODO: do not use, this will likely be removed later!*/
public boolean refreshOnSelect = false;
/** Key to press for this command. */
public @Nullable KeyBind keybind = null;
/** Extra stances that are available when this command is selected. These ignore incompatibleStances. */
public Seq<UnitStance> extraStances = new Seq<>();
public UnitCommand(String name, String icon, Func<Unit, AIController> controller){
super(name);

View File

@@ -12,7 +12,7 @@ import mindustry.input.*;
import mindustry.type.*;
public class UnitStance extends MappableContent{
public static UnitStance stop, holdFire, pursueTarget, patrol, ram, boost, mineAuto;
public static UnitStance stop, holdFire, pursueTarget, patrol, ram, boost, holdPosition, mineAuto;
/** Name of UI icon (from Icon class). */
public String icon;
@@ -84,16 +84,22 @@ public class UnitStance extends MappableContent{
stop = new UnitStance("stop", "cancel", Binding.cancelOrders, false);
holdFire = new UnitStance("holdfire", "none", Binding.unitStanceHoldFire);
pursueTarget = new UnitStance("pursuetarget", "right", Binding.unitStancePursueTarget);
patrol = new UnitStance("patrol", "refresh", Binding.unitStancePatrol);
patrol = new UnitStance("patrol", "refresh", Binding.unitStancePatrol){{
incompatibleCommands.addAll(UnitCommand.repairCommand, UnitCommand.assistCommand, UnitCommand.rebuildCommand);
}};
ram = new UnitStance("ram", "rightOpen", Binding.unitStanceRam);
boost = new UnitStance("boost", "up", Binding.unitStanceBoost){{
incompatibleCommands.addAll(UnitCommand.rebuildCommand, UnitCommand.repairCommand, UnitCommand.assistCommand);
}};
holdPosition = new UnitStance("holdposition", "effect", Binding.unitStanceHoldPosition);
mineAuto = new UnitStance("mineauto", "settings", null, false);
//Only vanilla items are supported for now
for(Item item : Vars.content.items()){
new ItemUnitStance(item);
}
Seq.with(UnitCommand.repairCommand, UnitCommand.assistCommand, UnitCommand.rebuildCommand)
.each(c -> c.extraStances.add(holdPosition));
}
}

View File

@@ -2,6 +2,7 @@ package mindustry.ai.types;
import arc.struct.*;
import arc.util.*;
import mindustry.ai.*;
import mindustry.entities.*;
import mindustry.entities.units.*;
import mindustry.game.Teams.*;
@@ -61,6 +62,7 @@ public class BuilderAI extends AIController{
}
boolean moving = false;
boolean hold = hasStance(UnitStance.holdPosition);
if(following != null){
retreatTimer = 0f;
@@ -77,7 +79,7 @@ public class BuilderAI extends AIController{
unit.plans.clear();
unit.plans.addFirst(following.buildPlan());
lastPlan = null;
}else if(unit.buildPlan() == null || alwaysFlee){
}else if((unit.buildPlan() == null || alwaysFlee) && !hold){
//not following anyone or building
if(timer.get(timerTarget4, 40)){
enemy = target(unit.x, unit.y, fleeRange, true, true);
@@ -121,10 +123,16 @@ public class BuilderAI extends AIController{
Build.validPlace(req.block, unit.team(), req.x, req.y, req.rotation)));
if(valid){
float range = Math.min(unit.type.buildRange - unit.type.hitSize * 2f, buildRadius);
//move toward the plan
moveTo(req.tile(), range, 20f);
moving = !unit.within(req.tile(), range);
if(!hold){
float range = Math.min(unit.type.buildRange - unit.type.hitSize * 2f, buildRadius);
//move toward the plan
moveTo(req.tile(), range, 20f);
moving = !unit.within(req.tile(), range);
}else if(!unit.within(req, unit.type.buildRange - tilesize) && !state.rules.infiniteResources){
//discard the plan, it's too far away to reach while holding position. try the next one
unit.plans.removeFirst();
lastPlan = null;
}
}else{
//discard invalid plan
unit.plans.removeFirst();
@@ -132,7 +140,7 @@ public class BuilderAI extends AIController{
}
}else{
if(assistFollowing != null){
if(assistFollowing != null && !hold){
moveTo(assistFollowing, assistFollowing.type.hitSize + unit.type.hitSize/2f + 60f);
moving = !unit.within(assistFollowing, assistFollowing.type.hitSize + unit.type.hitSize/2f + 65f);
}
@@ -179,21 +187,43 @@ public class BuilderAI extends AIController{
//find new plan
if(!onlyAssist && !unit.team.data().plans.isEmpty() && following == null && timer.get(timerTarget3, rebuildPeriod)){
Queue<BlockPlan> blocks = unit.team.data().plans;
BlockPlan block = blocks.first();
var blocks = unit.team.data().plans;
//check if it's already been placed
if(world.tile(block.x, block.y) != null && world.tile(block.x, block.y).block() == block.block){
blocks.removeFirst();
}else if(Build.validPlace(block.block, unit.team(), block.x, block.y, block.rotation) && (!alwaysFlee || !nearEnemy(block.x, block.y))){ //it's valid
lastPlan = block;
//add build plan
unit.addBuild(new BuildPlan(block.x, block.y, block.rotation, block.block, block.config));
//shift build plan to tail so next unit builds something else
blocks.addLast(blocks.removeFirst());
if(hold){
//essentially build turret behavior (find first plan in range)
for(int i = 0; i < blocks.size; i++){
var block = blocks.get(i);
if(state.rules.infiniteResources || unit.within(block.x * tilesize, block.y * tilesize, unit.type.buildRange)){
var btype = block.block;
if(Build.validPlace(btype, unit.team(), block.x, block.y, block.rotation)){
unit.addBuild(new BuildPlan(block.x, block.y, block.rotation, block.block, block.config));
//shift build plan to tail so next unit builds something else
blocks.addLast(blocks.removeIndex(i));
lastPlan = block;
break;
}
}
}
}else{
//shift head of queue to tail, try something else next time
blocks.addLast(blocks.removeFirst());
BlockPlan block = blocks.first();
//check if it's already been placed
if(world.tile(block.x, block.y) != null && world.tile(block.x, block.y).block() == block.block){
blocks.removeFirst();
}else if(Build.validPlace(block.block, unit.team(), block.x, block.y, block.rotation)
&& (!alwaysFlee || !nearEnemy(block.x, block.y))){ //check if it's valid
lastPlan = block;
//add build plan
unit.addBuild(new BuildPlan(block.x, block.y, block.rotation, block.block, block.config));
//shift build plan to tail so next unit builds something else
blocks.addLast(blocks.removeFirst());
}else{
//shift head of queue to tail, try something else next time
blocks.addLast(blocks.removeFirst());
}
}
}
}

View File

@@ -63,6 +63,7 @@ public class CommandAI extends AIController{
}
}
@Override
public boolean hasStance(@Nullable UnitStance stance){
return stance != null && stances.get(stance.id);
}

View File

@@ -1,6 +1,7 @@
package mindustry.ai.types;
import arc.util.*;
import mindustry.ai.*;
import mindustry.entities.*;
import mindustry.entities.units.*;
import mindustry.gen.*;
@@ -28,11 +29,15 @@ public class RepairAI extends AIController{
unit.controlWeapons(false);
}
boolean hold = hasStance(UnitStance.holdPosition);
if(target != null && target instanceof Building b && b.team == unit.team){
if(unit.type.circleTarget){
circleAttack(unit.type.circleTargetRadius);
}else if(!target.within(unit, unit.type.range * 0.65f)){
moveTo(target, unit.type.range * 0.65f);
if(!hold){
if(unit.type.circleTarget){
circleAttack(unit.type.circleTargetRadius);
}else if(!target.within(unit, unit.type.range * 0.65f)){
moveTo(target, unit.type.range * 0.65f);
}
}
if(!unit.type.circleTarget){
@@ -41,7 +46,7 @@ public class RepairAI extends AIController{
}
//not repairing
if(!(target instanceof Building)){
if(!(target instanceof Building) && !hold){
if(timer.get(timerTarget4, 40)){
avoid = target(unit.x, unit.y, fleeRange, true, true);
}

View File

@@ -4,6 +4,7 @@ import arc.math.*;
import arc.math.geom.*;
import arc.util.*;
import mindustry.*;
import mindustry.ai.*;
import mindustry.async.*;
import mindustry.entities.*;
import mindustry.game.*;
@@ -53,6 +54,13 @@ public class AIController implements UnitController{
updateMovement();
}
public boolean hasStance(@Nullable UnitStance stance){
if(unit.controller() instanceof AIController ai){
return ai.hasStance(stance);
}
return false;
}
/** Called when the parent CommandAI changes its stance. */
public void stanceChanged(){}

View File

@@ -52,6 +52,7 @@ public class Binding{
unitStancePatrol = KeyBind.add("unit_stance_patrol", KeyCode.unset),
unitStanceRam = KeyBind.add("unit_stance_ram", KeyCode.unset),
unitStanceBoost = KeyBind.add("unit_stance_boost", KeyCode.unset),
unitStanceHoldPosition = KeyBind.add("unit_stance_hold_position", KeyCode.unset),
unitCommandMove = KeyBind.add("unit_command_move", KeyCode.unset),
unitCommandRepair = KeyBind.add("unit_command_repair", KeyCode.unset),

View File

@@ -648,8 +648,12 @@ public class UnitType extends UnlockableContent implements Senseable{
/** Adds all available unit stances based on the unit's current state. This can change based on the command of the unit. */
public void getUnitStances(Unit unit, Seq<UnitStance> out){
if(!(unit.controller() instanceof CommandAI ai)) return;
var current = ai.currentCommand();
//return mining stances based on present items
if(unit.controller() instanceof CommandAI ai && ai.currentCommand() == UnitCommand.mineCommand){
if(current == UnitCommand.mineCommand){
out.add(UnitStance.mineAuto);
for(Item item : indexer.getAllPresentOres()){
if(unit.canMine(item) && ((mineFloor && indexer.hasOre(item)) || (mineWalls && indexer.hasWallOre(item)))){
@@ -660,13 +664,15 @@ public class UnitType extends UnlockableContent implements Senseable{
}
}
}else{
var command = unit.controller() instanceof CommandAI ai ? ai.command : null;
for(var stance : stances){
if(stance.isCompatible(command)){
if(stance.isCompatible(current)){
out.add(stance);
}
}
}
//there might be duplicates, but that shouldn't cause issues
out.addAll(current.extraStances);
}
public boolean allowStance(Unit unit, UnitStance stance){

View File

@@ -35,7 +35,6 @@ task incrementConfig{
dependencies{
implementation project(":core")
implementation arcModule("natives:natives-ios")
implementation arcModule("natives:natives-freetype-ios")
implementation arcModule("backends:backend-robovm")