diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index e4e2e03e57..82b40bc459 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -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 diff --git a/core/src/mindustry/ai/UnitCommand.java b/core/src/mindustry/ai/UnitCommand.java index 0e4bf2fd21..9eefeed7e2 100644 --- a/core/src/mindustry/ai/UnitCommand.java +++ b/core/src/mindustry/ai/UnitCommand.java @@ -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 extraStances = new Seq<>(); public UnitCommand(String name, String icon, Func controller){ super(name); diff --git a/core/src/mindustry/ai/UnitStance.java b/core/src/mindustry/ai/UnitStance.java index 68985c1131..424675e83e 100644 --- a/core/src/mindustry/ai/UnitStance.java +++ b/core/src/mindustry/ai/UnitStance.java @@ -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)); } } diff --git a/core/src/mindustry/ai/types/BuilderAI.java b/core/src/mindustry/ai/types/BuilderAI.java index 7bbf1dfa32..1c89b75ab8 100644 --- a/core/src/mindustry/ai/types/BuilderAI.java +++ b/core/src/mindustry/ai/types/BuilderAI.java @@ -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 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()); + } } } } diff --git a/core/src/mindustry/ai/types/CommandAI.java b/core/src/mindustry/ai/types/CommandAI.java index 949868a573..a39760337a 100644 --- a/core/src/mindustry/ai/types/CommandAI.java +++ b/core/src/mindustry/ai/types/CommandAI.java @@ -63,6 +63,7 @@ public class CommandAI extends AIController{ } } + @Override public boolean hasStance(@Nullable UnitStance stance){ return stance != null && stances.get(stance.id); } diff --git a/core/src/mindustry/ai/types/RepairAI.java b/core/src/mindustry/ai/types/RepairAI.java index bf3e9782ff..c46b15e221 100644 --- a/core/src/mindustry/ai/types/RepairAI.java +++ b/core/src/mindustry/ai/types/RepairAI.java @@ -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); } diff --git a/core/src/mindustry/entities/units/AIController.java b/core/src/mindustry/entities/units/AIController.java index 61f3ce7bd5..c774f72f75 100644 --- a/core/src/mindustry/entities/units/AIController.java +++ b/core/src/mindustry/entities/units/AIController.java @@ -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(){} diff --git a/core/src/mindustry/input/Binding.java b/core/src/mindustry/input/Binding.java index d4cf9d5140..bc12faeab0 100644 --- a/core/src/mindustry/input/Binding.java +++ b/core/src/mindustry/input/Binding.java @@ -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), diff --git a/core/src/mindustry/type/UnitType.java b/core/src/mindustry/type/UnitType.java index b8c47b49e8..c5dcf4d7a8 100644 --- a/core/src/mindustry/type/UnitType.java +++ b/core/src/mindustry/type/UnitType.java @@ -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 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){ diff --git a/ios/build.gradle b/ios/build.gradle index f61a602c9b..088c7a7014 100644 --- a/ios/build.gradle +++ b/ios/build.gradle @@ -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")