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.holdfire = Stance: Hold Fire
stance.pursuetarget = Stance: Pursue Target stance.pursuetarget = Stance: Pursue Target
stance.patrol = Stance: Patrol Path stance.patrol = Stance: Patrol Path
stance.holdposition = Stance: Hold Position
stance.ram = Stance: Ram\n[lightgray]Straight line movement, no pathfinding stance.ram = Stance: Ram\n[lightgray]Straight line movement, no pathfinding
stance.boost = Boost stance.boost = Boost
stance.mineauto = Automatic Mining 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_patrol.name = Unit Stance: Patrol
keybind.unit_stance_ram.name = Unit Stance: Ram keybind.unit_stance_ram.name = Unit Stance: Ram
keybind.unit_stance_boost.name = Unit Stance: Boost 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_move.name = Unit Command: Move
keybind.unit_command_repair.name = Unit Command: Repair keybind.unit_command_repair.name = Unit Command: Repair

View File

@@ -4,6 +4,7 @@ import arc.*;
import arc.func.*; import arc.func.*;
import arc.input.*; import arc.input.*;
import arc.scene.style.*; import arc.scene.style.*;
import arc.struct.*;
import arc.util.*; import arc.util.*;
import mindustry.ai.types.*; import mindustry.ai.types.*;
import mindustry.ctype.*; import mindustry.ctype.*;
@@ -27,12 +28,14 @@ public class UnitCommand extends MappableContent{
public boolean resetTarget = true; public boolean resetTarget = true;
/** Whether to snap the command destination to ally buildings. */ /** Whether to snap the command destination to ally buildings. */
public boolean snapToBuilding = false; public boolean snapToBuilding = false;
/** */ /** If true, the unit will arrive at this command's exact endpoint. */
public boolean exactArrival = false; 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!*/ /** 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; public boolean refreshOnSelect = false;
/** Key to press for this command. */ /** Key to press for this command. */
public @Nullable KeyBind keybind = null; 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){ public UnitCommand(String name, String icon, Func<Unit, AIController> controller){
super(name); super(name);

View File

@@ -12,7 +12,7 @@ import mindustry.input.*;
import mindustry.type.*; import mindustry.type.*;
public class UnitStance extends MappableContent{ 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). */ /** Name of UI icon (from Icon class). */
public String icon; public String icon;
@@ -84,16 +84,22 @@ public class UnitStance extends MappableContent{
stop = new UnitStance("stop", "cancel", Binding.cancelOrders, false); stop = new UnitStance("stop", "cancel", Binding.cancelOrders, false);
holdFire = new UnitStance("holdfire", "none", Binding.unitStanceHoldFire); holdFire = new UnitStance("holdfire", "none", Binding.unitStanceHoldFire);
pursueTarget = new UnitStance("pursuetarget", "right", Binding.unitStancePursueTarget); 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); ram = new UnitStance("ram", "rightOpen", Binding.unitStanceRam);
boost = new UnitStance("boost", "up", Binding.unitStanceBoost){{ boost = new UnitStance("boost", "up", Binding.unitStanceBoost){{
incompatibleCommands.addAll(UnitCommand.rebuildCommand, UnitCommand.repairCommand, UnitCommand.assistCommand); incompatibleCommands.addAll(UnitCommand.rebuildCommand, UnitCommand.repairCommand, UnitCommand.assistCommand);
}}; }};
holdPosition = new UnitStance("holdposition", "effect", Binding.unitStanceHoldPosition);
mineAuto = new UnitStance("mineauto", "settings", null, false); mineAuto = new UnitStance("mineauto", "settings", null, false);
//Only vanilla items are supported for now //Only vanilla items are supported for now
for(Item item : Vars.content.items()){ for(Item item : Vars.content.items()){
new ItemUnitStance(item); 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.struct.*;
import arc.util.*; import arc.util.*;
import mindustry.ai.*;
import mindustry.entities.*; import mindustry.entities.*;
import mindustry.entities.units.*; import mindustry.entities.units.*;
import mindustry.game.Teams.*; import mindustry.game.Teams.*;
@@ -61,6 +62,7 @@ public class BuilderAI extends AIController{
} }
boolean moving = false; boolean moving = false;
boolean hold = hasStance(UnitStance.holdPosition);
if(following != null){ if(following != null){
retreatTimer = 0f; retreatTimer = 0f;
@@ -77,7 +79,7 @@ public class BuilderAI extends AIController{
unit.plans.clear(); unit.plans.clear();
unit.plans.addFirst(following.buildPlan()); unit.plans.addFirst(following.buildPlan());
lastPlan = null; lastPlan = null;
}else if(unit.buildPlan() == null || alwaysFlee){ }else if((unit.buildPlan() == null || alwaysFlee) && !hold){
//not following anyone or building //not following anyone or building
if(timer.get(timerTarget4, 40)){ if(timer.get(timerTarget4, 40)){
enemy = target(unit.x, unit.y, fleeRange, true, true); 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))); Build.validPlace(req.block, unit.team(), req.x, req.y, req.rotation)));
if(valid){ if(valid){
float range = Math.min(unit.type.buildRange - unit.type.hitSize * 2f, buildRadius); if(!hold){
//move toward the plan float range = Math.min(unit.type.buildRange - unit.type.hitSize * 2f, buildRadius);
moveTo(req.tile(), range, 20f); //move toward the plan
moving = !unit.within(req.tile(), range); 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{ }else{
//discard invalid plan //discard invalid plan
unit.plans.removeFirst(); unit.plans.removeFirst();
@@ -132,7 +140,7 @@ public class BuilderAI extends AIController{
} }
}else{ }else{
if(assistFollowing != null){ if(assistFollowing != null && !hold){
moveTo(assistFollowing, assistFollowing.type.hitSize + unit.type.hitSize/2f + 60f); moveTo(assistFollowing, assistFollowing.type.hitSize + unit.type.hitSize/2f + 60f);
moving = !unit.within(assistFollowing, assistFollowing.type.hitSize + unit.type.hitSize/2f + 65f); moving = !unit.within(assistFollowing, assistFollowing.type.hitSize + unit.type.hitSize/2f + 65f);
} }
@@ -179,21 +187,43 @@ public class BuilderAI extends AIController{
//find new plan //find new plan
if(!onlyAssist && !unit.team.data().plans.isEmpty() && following == null && timer.get(timerTarget3, rebuildPeriod)){ if(!onlyAssist && !unit.team.data().plans.isEmpty() && following == null && timer.get(timerTarget3, rebuildPeriod)){
Queue<BlockPlan> blocks = unit.team.data().plans; var blocks = unit.team.data().plans;
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){ if(hold){
blocks.removeFirst(); //essentially build turret behavior (find first plan in range)
}else if(Build.validPlace(block.block, unit.team(), block.x, block.y, block.rotation) && (!alwaysFlee || !nearEnemy(block.x, block.y))){ //it's valid for(int i = 0; i < blocks.size; i++){
lastPlan = block; var block = blocks.get(i);
//add build plan if(state.rules.infiniteResources || unit.within(block.x * tilesize, block.y * tilesize, unit.type.buildRange)){
unit.addBuild(new BuildPlan(block.x, block.y, block.rotation, block.block, block.config)); var btype = block.block;
//shift build plan to tail so next unit builds something else
blocks.addLast(blocks.removeFirst()); 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{ }else{
//shift head of queue to tail, try something else next time BlockPlan block = blocks.first();
blocks.addLast(blocks.removeFirst());
//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){ public boolean hasStance(@Nullable UnitStance stance){
return stance != null && stances.get(stance.id); return stance != null && stances.get(stance.id);
} }

View File

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

View File

@@ -4,6 +4,7 @@ import arc.math.*;
import arc.math.geom.*; import arc.math.geom.*;
import arc.util.*; import arc.util.*;
import mindustry.*; import mindustry.*;
import mindustry.ai.*;
import mindustry.async.*; import mindustry.async.*;
import mindustry.entities.*; import mindustry.entities.*;
import mindustry.game.*; import mindustry.game.*;
@@ -53,6 +54,13 @@ public class AIController implements UnitController{
updateMovement(); 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. */ /** Called when the parent CommandAI changes its stance. */
public void stanceChanged(){} public void stanceChanged(){}

View File

@@ -52,6 +52,7 @@ public class Binding{
unitStancePatrol = KeyBind.add("unit_stance_patrol", KeyCode.unset), unitStancePatrol = KeyBind.add("unit_stance_patrol", KeyCode.unset),
unitStanceRam = KeyBind.add("unit_stance_ram", KeyCode.unset), unitStanceRam = KeyBind.add("unit_stance_ram", KeyCode.unset),
unitStanceBoost = KeyBind.add("unit_stance_boost", 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), unitCommandMove = KeyBind.add("unit_command_move", KeyCode.unset),
unitCommandRepair = KeyBind.add("unit_command_repair", 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. */ /** 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){ 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 //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); out.add(UnitStance.mineAuto);
for(Item item : indexer.getAllPresentOres()){ for(Item item : indexer.getAllPresentOres()){
if(unit.canMine(item) && ((mineFloor && indexer.hasOre(item)) || (mineWalls && indexer.hasWallOre(item)))){ if(unit.canMine(item) && ((mineFloor && indexer.hasOre(item)) || (mineWalls && indexer.hasWallOre(item)))){
@@ -660,13 +664,15 @@ public class UnitType extends UnlockableContent implements Senseable{
} }
} }
}else{ }else{
var command = unit.controller() instanceof CommandAI ai ? ai.command : null;
for(var stance : stances){ for(var stance : stances){
if(stance.isCompatible(command)){ if(stance.isCompatible(current)){
out.add(stance); out.add(stance);
} }
} }
} }
//there might be duplicates, but that shouldn't cause issues
out.addAll(current.extraStances);
} }
public boolean allowStance(Unit unit, UnitStance stance){ public boolean allowStance(Unit unit, UnitStance stance){

View File

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