1
0
mirror of https://github.com/Anuken/Mindustry.git synced 2024-10-06 04:47:14 +03:00

WIP command order system

This commit is contained in:
Anuken 2022-07-30 21:01:07 -04:00
parent a326e36bbe
commit 55edd53f84
11 changed files with 327 additions and 62 deletions

View File

@ -311,6 +311,11 @@ open = Open
customize = Customize Rules
cancel = Cancel
command = Command
command.mine = Mine
command.repair = Repair
command.rebuild = Rebuild
command.assist = Assist Player
command.move = Move
openlink = Open Link
copylink = Copy Link
back = Back

View File

@ -0,0 +1,52 @@
package mindustry.ai;
import arc.*;
import arc.func.*;
import arc.struct.*;
import mindustry.ai.types.*;
import mindustry.entities.units.*;
import mindustry.gen.*;
/** Defines a pattern of behavior that an RTS-controlled unit should follow. Shows up in the command UI. */
public class UnitCommand{
/** List of all commands by ID. */
public static final Seq<UnitCommand> all = new Seq<>();
public static final UnitCommand
//TODO they do not use the command "interface" or designation at all
moveCommand = new UnitCommand("move", "right", u -> null),
repairCommand = new UnitCommand("repair", "modeSurvival", u -> new RepairAI()),
rebuildCommand = new UnitCommand("rebuild", "hammer", u -> new BuilderAI()),
assistCommand = new UnitCommand("assist", "players", u -> {
var ai = new BuilderAI();
ai.onlyAssist = true;
return ai;
}),
mineCommand = new UnitCommand("mine", "production", u -> new MinerAI());
/** Default set of specified commands. */
public static final UnitCommand[] defaultCommands = {moveCommand};
/** Unique ID number. */
public final int id;
/** Named used for tooltip/description. */
public final String name;
/** Name of UI icon (from Icon class). */
public final String icon;
/** Controller that this unit will use when this command is used. Return null for "default" behavior. */
public final Func<Unit, AIController> controller;
public UnitCommand(String name, String icon, Func<Unit, AIController> controller){
this.name = name;
this.icon = icon;
this.controller = controller;
id = all.size;
all.add(this);
}
public String localized(){
return Core.bundle.get("command." + name);
}
}

View File

@ -14,12 +14,14 @@ import static mindustry.Vars.*;
public class BuilderAI extends AIController{
public static float buildRadius = 1500, retreatDst = 110f, retreatDelay = Time.toSeconds * 2f;
public @Nullable Unit assistFollowing;
public @Nullable Unit following;
public @Nullable Teamc enemy;
public @Nullable BlockPlan lastPlan;
public float fleeRange = 370f;
public boolean alwaysFlee;
public boolean onlyAssist;
boolean found = false;
float retreatTimer;
@ -41,6 +43,10 @@ public class BuilderAI extends AIController{
unit.updateBuilding = true;
if(assistFollowing != null && assistFollowing.activelyBuilding()){
following = assistFollowing;
}
if(following != null){
retreatTimer = 0f;
//try to follow and mimic someone
@ -108,6 +114,10 @@ public class BuilderAI extends AIController{
}
}else{
if(assistFollowing != null){
moveTo(assistFollowing, assistFollowing.type.hitSize * 1.5f + 60f);
}
//follow someone and help them build
if(timer.get(timerTarget2, 60f)){
found = false;
@ -130,13 +140,29 @@ public class BuilderAI extends AIController{
}
}
});
if(onlyAssist){
float minDst = Float.MAX_VALUE;
Player closest = null;
for(var player : Groups.player){
if(player.unit().canBuild() && !player.dead()){
float dst = player.dst2(unit);
if(dst < minDst){
closest = player;
minDst = dst;
}
}
}
assistFollowing = closest == null ? null : closest.unit();
}
}
//TODO this is bad, rebuild time should not depend on AI here
float rebuildTime = (unit.team.rules().rtsAi ? 12f : 2f) * 60f;
//find new plan
if(!unit.team.data().plans.isEmpty() && following == null && timer.get(timerTarget3, rebuildTime)){
if(!onlyAssist && !unit.team.data().plans.isEmpty() && following == null && timer.get(timerTarget3, rebuildTime)){
Queue<BlockPlan> blocks = unit.team.data().plans;
BlockPlan block = blocks.first();

View File

@ -12,20 +12,61 @@ import mindustry.gen.*;
import mindustry.world.*;
public class CommandAI extends AIController{
private static final float localInterval = 40f;
private static final Vec2 vecOut = new Vec2(), flockVec = new Vec2(), separation = new Vec2(), cohesion = new Vec2(), massCenter = new Vec2();
protected static final float localInterval = 40f;
protected static final Vec2 vecOut = new Vec2(), flockVec = new Vec2(), separation = new Vec2(), cohesion = new Vec2(), massCenter = new Vec2();
public @Nullable Vec2 targetPos;
public @Nullable Teamc attackTarget;
private boolean stopAtTarget;
private Vec2 lastTargetPos;
private int pathId = -1;
private Seq<Unit> local = new Seq<>(false);
private boolean flocked;
protected boolean stopAtTarget;
protected Vec2 lastTargetPos;
protected int pathId = -1;
protected Seq<Unit> local = new Seq<>(false);
protected boolean flocked;
/** Current command this unit is following. */
public @Nullable UnitCommand command;
/** Current controller instance based on command. */
protected @Nullable AIController commandController;
/** Last command type assigned. Used for detecting command changes. */
protected @Nullable UnitCommand lastCommand;
public @Nullable UnitCommand currentCommand(){
return command;
}
/** Attempts to assign a command to this unit. If not supported by the unit type, does nothing. */
public void command(UnitCommand command){
if(Structs.contains(unit.type.commands, command)){
//clear old state.
unit.mineTile = null;
unit.clearBuilding();
this.command = command;
}
}
@Override
public void updateUnit(){
//assign defaults
if(command == null && unit.type.commands.length > 0){
command = unit.type.defaultCommand == null ? unit.type.commands[0] : unit.type.defaultCommand;
}
//update command controller based on index.
var curCommand = currentCommand();
if(lastCommand != curCommand){
lastCommand = curCommand;
commandController = (curCommand == null ? null : curCommand.controller.get(unit));
}
//use the command controller if it is provided, and bail out.
if(commandController != null){
if(commandController.unit() != unit) commandController.unit(unit);
commandController.updateUnit();
return;
}
updateVisuals();
updateTargeting();
@ -104,6 +145,7 @@ public class CommandAI extends AIController{
}
if(attackTarget == null){
//TODO overshoot.
if(unit.within(targetPos, Math.max(5f, unit.hitSize / 2f))){
targetPos = null;
}else if(local.size > 1){
@ -134,10 +176,55 @@ public class CommandAI extends AIController{
}
}
@Override
public boolean keepState(){
return true;
}
@Override
public Teamc findTarget(float x, float y, float range, boolean air, boolean ground){
return attackTarget == null || !attackTarget.within(x, y, range + 3f + (attackTarget instanceof Sized s ? s.hitSize()/2f : 0f)) ? super.findTarget(x, y, range, air, ground) : attackTarget;
}
@Override
public boolean retarget(){
//retarget faster when there is an explicit target
return attackTarget != null ? timer.get(timerTarget, 10) : timer.get(timerTarget, 20);
}
public boolean hasCommand(){
return targetPos != null;
}
public void setupLastPos(){
lastTargetPos = targetPos;
}
public void commandPosition(Vec2 pos){
targetPos = pos;
lastTargetPos = pos;
attackTarget = null;
pathId = Vars.controlPath.nextTargetId();
}
public void commandTarget(Teamc moveTo){
commandTarget(moveTo, false);
}
public void commandTarget(Teamc moveTo, boolean stopAtTarget){
attackTarget = moveTo;
this.stopAtTarget = stopAtTarget;
pathId = Vars.controlPath.nextTargetId();
}
/*
//TODO ひどい
(does not work)
public static float cohesionScl = 0.3f;
public static float cohesionRad = 3f, separationRad = 1.1f, separationScl = 1f, flockMult = 0.5f;
//TODO ひどい
Vec2 calculateFlock(){
if(local.isEmpty()) return flockVec.setZero();
@ -177,47 +264,5 @@ public class CommandAI extends AIController{
}
return flockVec;
}
@Override
public boolean keepState(){
return true;
}
@Override
public Teamc findTarget(float x, float y, float range, boolean air, boolean ground){
return attackTarget == null || !attackTarget.within(x, y, range + 3f + (attackTarget instanceof Sized s ? s.hitSize()/2f : 0f)) ? super.findTarget(x, y, range, air, ground) : attackTarget;
}
@Override
public boolean retarget(){
//retarget faster when there is an explicit target
return attackTarget != null ? timer.get(timerTarget, 10) : timer.get(timerTarget, 20);
}
public boolean hasCommand(){
return targetPos != null;
}
public void setupLastPos(){
lastTargetPos = targetPos;
}
public void commandPosition(Vec2 pos){
targetPos = pos;
lastTargetPos = pos;
attackTarget = null;
pathId = Vars.controlPath.nextTargetId();
}
public void commandTarget(Teamc moveTo){
commandTarget(moveTo, false);
}
public void commandTarget(Teamc moveTo, boolean stopAtTarget){
attackTarget = moveTo;
this.stopAtTarget = stopAtTarget;
pathId = Vars.controlPath.nextTargetId();
}
}*/
}

View File

@ -6,6 +6,7 @@ import arc.math.*;
import arc.math.geom.*;
import arc.struct.*;
import arc.util.*;
import mindustry.ai.*;
import mindustry.ai.types.*;
import mindustry.annotations.Annotations.*;
import mindustry.entities.*;
@ -1247,8 +1248,11 @@ public class UnitTypes{
//region air support
mono = new UnitType("mono"){{
//there's no reason to command monos anywhere. it's just annoying.
controller = u -> new MinerAI();
defaultCommand = UnitCommand.mineCommand;
flying = true;
drag = 0.06f;
accel = 0.12f;
@ -1266,7 +1270,7 @@ public class UnitTypes{
}};
poly = new UnitType("poly"){{
controller = u -> new BuilderAI();
defaultCommand = UnitCommand.rebuildCommand;
flying = true;
drag = 0.05f;
@ -1320,7 +1324,7 @@ public class UnitTypes{
}};
mega = new UnitType("mega"){{
controller = u -> new RepairAI();
defaultCommand = UnitCommand.repairCommand;
mineTier = 3;
mineSpeed = 4f;

View File

@ -82,6 +82,8 @@ abstract class BuilderComp implements Posc, Statusc, Teamc, Rotc{
boolean infinite = state.rules.infiniteResources || team().rules().infiniteResources;
buildCounter += Time.delta;
if(Float.isNaN(buildCounter) || Float.isInfinite(buildCounter)) buildCounter = 0f;
buildCounter = Math.min(buildCounter, 10f);
while(buildCounter >= 1){
buildCounter -= 1f;

View File

@ -14,6 +14,7 @@ import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.ai.*;
import mindustry.ai.types.*;
import mindustry.annotations.Annotations.*;
import mindustry.content.*;
@ -228,6 +229,10 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
for(int id : unitIds){
Unit unit = Groups.unit.getByID(id);
if(unit != null && unit.team == player.team() && unit.controller() instanceof CommandAI ai){
//implicitly order it to move
ai.command(UnitCommand.moveCommand);
if(teamTarget != null && teamTarget.team() != player.team()){
ai.commandTarget(teamTarget);
}else if(posTarget != null){
@ -246,6 +251,28 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
}
}
@Remote(called = Loc.server, targets = Loc.both, forward = true)
public static void setUnitCommand(Player player, int[] unitIds, UnitCommand command){
if(player == null || unitIds == null || command == null) return;
if(net.server() && !netServer.admins.allowAction(player, ActionType.commandUnits, event -> {
event.unitIDs = unitIds;
})){
throw new ValidateException(player, "Player cannot command units.");
}
for(int id : unitIds){
Unit unit = Groups.unit.getByID(id);
if(unit != null && unit.team == player.team() && unit.controller() instanceof CommandAI ai){
ai.command(command);
//reset targeting
ai.targetPos = null;
ai.attackTarget = null;
unit.lastCommanded = player.coloredName();
}
}
}
@Remote(called = Loc.server, targets = Loc.both, forward = true)
public static void commandBuilding(Player player, Building build, Vec2 target){
if(player == null || build == null || build.team != player.team() || !build.block.commandable || target == null) return;
@ -814,7 +841,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
for(Unit unit : selectedUnits){
CommandAI ai = unit.command();
//draw target line
if(ai.targetPos != null){
if(ai.targetPos != null && ai.command == UnitCommand.moveCommand){
Position lineDest = ai.attackTarget != null ? ai.attackTarget : ai.targetPos;
Drawf.limitLine(unit, lineDest, unit.hitSize / 2f, 3.5f);
@ -825,7 +852,8 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
Drawf.square(unit.x, unit.y, unit.hitSize / 1.4f + 1f);
if(ai.attackTarget != null){
//TODO when to draw, when to not?
if(ai.attackTarget != null && ai.command == UnitCommand.moveCommand){
Drawf.target(ai.attackTarget.getX(), ai.attackTarget.getY(), 6f, Pal.remove);
}
}

View File

@ -6,6 +6,7 @@ import arc.math.geom.*;
import arc.struct.*;
import arc.util.*;
import arc.util.io.*;
import mindustry.ai.*;
import mindustry.ai.types.*;
import mindustry.annotations.Annotations.*;
import mindustry.content.TechTree.*;
@ -283,6 +284,14 @@ public class TypeIO{
return Nulls.unit;
}
public static void writeCommand(Writes write, UnitCommand command){
write.b(command.id);
}
public static UnitCommand readCommand(Reads read){
return UnitCommand.all.get(read.ub());
}
public static void writeEntity(Writes write, Entityc entity){
write.i(entity == null ? -1 : entity.id());
}
@ -441,7 +450,7 @@ public class TypeIO{
write.b(3);
write.i(logic.controller.pos());
}else if(control instanceof CommandAI ai){
write.b(4);
write.b(6);
write.bool(ai.attackTarget != null);
write.bool(ai.targetPos != null);
@ -457,6 +466,7 @@ public class TypeIO{
write.i(((Unit)ai.attackTarget).id);
}
}
write.b(ai.command == null ? -1 : ai.command.id);
}else if(control instanceof AssemblerAI){ //hate
write.b(5);
}else{
@ -488,7 +498,8 @@ public class TypeIO{
out.controller = world.build(pos);
return out;
}
}else if(type == 4){
//type 4 is the old CommandAI with no commandIndex, type 6 is the new one with the index as a single byte.
}else if(type == 4 || type == 6){
CommandAI ai = prev instanceof CommandAI pai ? pai : new CommandAI();
boolean hasAttack = read.bool(), hasPos = read.bool();
@ -511,6 +522,11 @@ public class TypeIO{
ai.attackTarget = null;
}
if(type == 6){
byte id = read.b();
ai.command = id < 0 ? null : UnitCommand.all.get(id);
}
return ai;
}else if(type == 5){
//augh

View File

@ -277,6 +277,11 @@ public class UnitType extends UnlockableContent{
/** Flags to target based on priority. Null indicates that the closest target should be found. The closest enemy core is used as a fallback. */
public BlockFlag[] targetFlags = {null};
/** Commands available to this unit through RTS controls. An empty array means commands will be assigned based on unit capabilities in init(). */
public UnitCommand[] commands = {};
/** Command to assign to this unit upon creation. Null indicates the first command in the array. */
public @Nullable UnitCommand defaultCommand;
/** color for outline generated around sprites */
public Color outlineColor = Pal.darkerMetal;
/** thickness for sprite outline */
@ -772,6 +777,33 @@ public class UnitType extends UnlockableContent{
canAttack = weapons.contains(w -> !w.noAttack);
//assign default commands.
if(commands.length == 0){
Seq<UnitCommand> cmds = new Seq<>(UnitCommand.class);
//TODO ????
//if(canAttack){
cmds.add(UnitCommand.moveCommand);
//}
//healing, mining and building is only supported for flying units; pathfinding to ambiguously reachable locations is hard.
if(flying){
if(canHeal){
cmds.add(UnitCommand.repairCommand);
}
if(buildSpeed > 0){
cmds.add(UnitCommand.rebuildCommand, UnitCommand.assistCommand);
}
if(mineSpeed > 0){
cmds.add(UnitCommand.mineCommand);
}
}
commands = cmds.toArray();
}
//dynamically create ammo capacity based on firing rate
if(ammoCapacity < 0){
float shotsPerSecond = weapons.sumf(w -> w.useAmmo ? 60f / w.reload : 0f);

View File

@ -11,6 +11,8 @@ import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.ai.*;
import mindustry.content.*;
import mindustry.core.*;
import mindustry.entities.*;
@ -422,6 +424,8 @@ public class PlacementFragment{
commandTable.table(u -> {
u.left();
int[] curCount = {0};
UnitCommand[] currentCommand = {null};
var commands = new Seq<UnitCommand>();
Runnable rebuildCommand = () -> {
u.clearChildren();
@ -431,12 +435,17 @@ public class PlacementFragment{
for(var unit : units){
counts[unit.type.id] ++;
}
commands.clear();
boolean firstCommand = false;
Table unitlist = u.table().growX().left().get();
unitlist.left();
int col = 0;
for(int i = 0; i < counts.length; i++){
if(counts[i] > 0){
var type = content.unit(i);
u.add(new ItemImage(type.uiIcon, counts[i])).tooltip(type.localizedName).pad(4).with(b -> {
ClickListener listener = new ClickListener();
unitlist.add(new ItemImage(type.uiIcon, counts[i])).tooltip(type.localizedName).pad(4).with(b -> {
var listener = new ClickListener();
//left click -> select
b.clicked(KeyCode.mouseLeft, () -> control.input.selectedUnits.removeAll(unit -> unit.type != type));
@ -450,16 +459,62 @@ public class PlacementFragment{
});
if(++col % 7 == 0){
u.row();
unitlist.row();
}
if(!firstCommand){
commands.add(type.commands);
firstCommand = true;
}else{
//remove commands that this next unit type doesn't have
commands.removeAll(com -> !Structs.contains(type.commands, com));
}
}
}
if(commands.size > 1){
u.row();
u.table(coms -> {
for(var command : commands){
coms.button(Icon.icons.get(command.icon, Icon.cancel), Styles.clearNoneTogglei, () -> {
IntSeq ids = new IntSeq();
for(var unit : units){
ids.add(unit.id);
}
Call.setUnitCommand(Vars.player, ids.toArray(), command);
}).checked(i -> currentCommand[0] == command).size(50f).tooltip(command.localized());
}
}).fillX().padTop(4f).left();
}
}else{
u.add("[no units]").color(Color.lightGray).growX().center().labelAlign(Align.center).pad(6);
}
};
u.update(() -> {
boolean hadCommand = false;
UnitCommand shareCommand = null;
//find the command that all units have, or null if they do not share one
for(var unit : control.input.selectedUnits){
if(unit.isCommandable()){
var nextCommand = unit.command().currentCommand();
if(hadCommand){
if(shareCommand != nextCommand){
shareCommand = null;
}
}else{
shareCommand = nextCommand;
hadCommand = true;
}
}
}
currentCommand[0] = shareCommand;
int size = control.input.selectedUnits.size;
if(curCount[0] != size){
curCount[0] = size;

View File

@ -25,4 +25,4 @@ org.gradle.caching=true
#used for slow jitpack builds; TODO see if this actually works
org.gradle.internal.http.socketTimeout=100000
org.gradle.internal.http.connectionTimeout=100000
archash=7d543096d5
archash=1477681512