From f9efbb6686f5a33640427b7ad732463bceaea5f5 Mon Sep 17 00:00:00 2001 From: Anuken Date: Tue, 8 Feb 2022 18:30:35 -0500 Subject: [PATCH] Unit command prototype --- core/src/mindustry/ai/types/CommandAI.java | 35 ++++++++++++ core/src/mindustry/content/Blocks.java | 3 +- core/src/mindustry/content/Fx.java | 6 ++ core/src/mindustry/core/Logic.java | 8 ++- .../entities/comp/CommanderComp.java | 2 +- .../mindustry/entities/comp/PlayerComp.java | 4 +- .../src/mindustry/entities/comp/UnitComp.java | 10 +++- core/src/mindustry/game/Rules.java | 2 + core/src/mindustry/game/Teams.java | 13 ++--- core/src/mindustry/input/Binding.java | 1 + core/src/mindustry/input/DesktopInput.java | 55 ++++++++++++++++++- core/src/mindustry/input/InputHandler.java | 15 ++++- .../maps/planet/ErekirPlanetGenerator.java | 1 + core/src/mindustry/type/UnitType.java | 8 +++ .../mindustry/type/unit/ErekirUnitType.java | 3 + 15 files changed, 146 insertions(+), 20 deletions(-) create mode 100644 core/src/mindustry/ai/types/CommandAI.java diff --git a/core/src/mindustry/ai/types/CommandAI.java b/core/src/mindustry/ai/types/CommandAI.java new file mode 100644 index 0000000000..43f881a41f --- /dev/null +++ b/core/src/mindustry/ai/types/CommandAI.java @@ -0,0 +1,35 @@ +package mindustry.ai.types; + +import arc.math.geom.*; +import arc.util.*; +import mindustry.entities.units.*; +import mindustry.gen.*; + +public class CommandAI extends AIController{ + public @Nullable Vec2 targetPos; + + @Override + public void updateUnit(){ + //TODO + + if(targetPos != null){ + //if(unit.isFlying()){ + moveTo(targetPos, 5f); + //} + + if(unit.isFlying()){ + unit.lookAt(targetPos); + }else{ + faceTarget(); + } + } + } + + public void commandPosition(Vec2 pos){ + targetPos = pos; + } + + public void commandTarget(Teamc moveTo){ + //TODO + } +} diff --git a/core/src/mindustry/content/Blocks.java b/core/src/mindustry/content/Blocks.java index bd8fe0a6cf..43a8e1ee89 100644 --- a/core/src/mindustry/content/Blocks.java +++ b/core/src/mindustry/content/Blocks.java @@ -3535,7 +3535,8 @@ public class Blocks{ size = 3; }}; - //TODO setup, sprite, balance... + //TODO setup, sprite, balance... or just scrap it completely. + if(false) droneCenter = new DroneCenter("drone-center"){{ requirements(Category.units, with(Items.graphite, 10)); diff --git a/core/src/mindustry/content/Fx.java b/core/src/mindustry/content/Fx.java index 114d186911..6f69c71448 100644 --- a/core/src/mindustry/content/Fx.java +++ b/core/src/mindustry/content/Fx.java @@ -183,6 +183,12 @@ public class Fx{ } }), + moveCommand = new Effect(15, e -> { + color(Pal.command); + stroke(e.fout() * 5f); + Lines.circle(e.x, e.y, 6f + e.fin() * 2f); + }).layer(Layer.effect - 20f), + commandSend = new Effect(28, e -> { color(Pal.command); stroke(e.fout() * 2f); diff --git a/core/src/mindustry/core/Logic.java b/core/src/mindustry/core/Logic.java index a18bdaad0d..06a14a63b6 100644 --- a/core/src/mindustry/core/Logic.java +++ b/core/src/mindustry/core/Logic.java @@ -4,6 +4,7 @@ import arc.*; import arc.math.*; import arc.util.*; import mindustry.annotations.Annotations.*; +import mindustry.content.*; import mindustry.core.GameState.*; import mindustry.ctype.*; import mindustry.game.EventType.*; @@ -114,11 +115,16 @@ public class Logic implements ApplicationListener{ if(state.isCampaign()){ //enable building AI on campaign unless the preset disables it - //TODO should be configurable, I don't want building AI everywhere. + //TODO should be (more) configurable, I don't want building AI everywhere. if(state.getSector().planet.defaultAI && !(state.getSector().preset != null && !state.getSector().preset.useAI)){ state.rules.waveTeam.rules().ai = true; } + //TODO unit commanding is not allowed on serpulo until I test it properly + if(state.getSector().planet != Planets.serpulo){ + state.rules.unitCommand = true; + } + state.rules.coreIncinerates = true; state.rules.waveTeam.rules().aiTier = state.getSector().threat * 0.8f; state.rules.waveTeam.rules().infiniteResources = true; diff --git a/core/src/mindustry/entities/comp/CommanderComp.java b/core/src/mindustry/entities/comp/CommanderComp.java index 693137935e..0f2ab77a8b 100644 --- a/core/src/mindustry/entities/comp/CommanderComp.java +++ b/core/src/mindustry/entities/comp/CommanderComp.java @@ -119,7 +119,7 @@ abstract class CommanderComp implements Entityc, Posc{ //reset controlled units for(Unit unit : controlling){ if(unit.controller().isBeingControlled(self())){ - unit.controller(unit.type.createController()); + unit.controller(unit.type.createController(unit)); } } diff --git a/core/src/mindustry/entities/comp/PlayerComp.java b/core/src/mindustry/entities/comp/PlayerComp.java index 38a43cae87..4761633486 100644 --- a/core/src/mindustry/entities/comp/PlayerComp.java +++ b/core/src/mindustry/entities/comp/PlayerComp.java @@ -82,7 +82,7 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra textFadeTime = 0f; x = y = 0f; if(!dead()){ - unit.controller(unit.type.createController()); + unit.resetController(); unit = Nulls.unit; } } @@ -203,7 +203,7 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra if(this.unit != Nulls.unit){ //un-control the old unit - this.unit.controller(this.unit.type.createController()); + this.unit.resetController(); } this.unit = unit; if(unit != Nulls.unit){ diff --git a/core/src/mindustry/entities/comp/UnitComp.java b/core/src/mindustry/entities/comp/UnitComp.java index 30911bec5a..4a34fe5e34 100644 --- a/core/src/mindustry/entities/comp/UnitComp.java +++ b/core/src/mindustry/entities/comp/UnitComp.java @@ -259,7 +259,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I } public void resetController(){ - controller(type.createController()); + controller(type.createController(self())); } @Override @@ -291,6 +291,10 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I return controller instanceof AIController; } + public boolean isCommandable(){ + return controller instanceof CommandAI; + } + public int count(){ return team.data().countType(type); } @@ -307,7 +311,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I this.hitSize = type.hitSize; this.hovering = type.hovering; - if(controller == null) controller(type.createController()); + if(controller == null) controller(type.createController(self())); if(mounts().length != type.weapons.size) setupWeapons(type); if(abilities.length != type.abilities.size){ abilities = new Ability[type.abilities.size]; @@ -328,7 +332,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I public void afterRead(){ afterSync(); //reset controller state - controller(type.createController()); + controller(type.createController(self())); } @Override diff --git a/core/src/mindustry/game/Rules.java b/core/src/mindustry/game/Rules.java index 6ff55ef14f..096b14abd7 100644 --- a/core/src/mindustry/game/Rules.java +++ b/core/src/mindustry/game/Rules.java @@ -47,6 +47,8 @@ public class Rules{ public boolean damageExplosions = true; /** Whether fire is enabled. */ public boolean fire = true; + /** Erekir-specific: If true, unit RTS controls can be used. */ + public boolean unitCommand = false; /** Whether units use and require ammo. */ public boolean unitAmmo = false; /** EXPERIMENTAL! If true, blocks will update in units and share power. */ diff --git a/core/src/mindustry/game/Teams.java b/core/src/mindustry/game/Teams.java index 8df1f5940b..2436c87e42 100644 --- a/core/src/mindustry/game/Teams.java +++ b/core/src/mindustry/game/Teams.java @@ -8,7 +8,6 @@ import arc.struct.*; import arc.util.*; import mindustry.*; import mindustry.ai.*; -import mindustry.content.*; import mindustry.entities.units.*; import mindustry.gen.*; import mindustry.type.*; @@ -236,23 +235,19 @@ public class Teams{ public UnitCommand command = UnitCommand.attack; /** Quadtree for all buildings of this team. Null if not active. */ - @Nullable - public QuadTree buildings; + public @Nullable QuadTree buildings; /** Current unit cap. Do not modify externally. */ public int unitCap; /** Total unit count. */ public int unitCount; /** Counts for each type of unit. Do not access directly. */ - @Nullable - public int[] typeCounts; + public @Nullable int[] typeCounts; /** Quadtree for units of this team. Do not access directly. */ - @Nullable - public QuadTree tree; + public @Nullable QuadTree tree; /** Units of this team. Updated each frame. */ public Seq units = new Seq<>(); /** Units of this team by type. Updated each frame. */ - @Nullable - public Seq[] unitsByType; + public @Nullable Seq[] unitsByType; public TeamData(Team team){ this.team = team; diff --git a/core/src/mindustry/input/Binding.java b/core/src/mindustry/input/Binding.java index de9aa56d04..9a9ff76062 100644 --- a/core/src/mindustry/input/Binding.java +++ b/core/src/mindustry/input/Binding.java @@ -12,6 +12,7 @@ public enum Binding implements KeyBind{ pan(KeyCode.mouseForward), boost(KeyCode.shiftLeft), + commandMode(KeyCode.shiftLeft), control(KeyCode.controlLeft), respawn(KeyCode.v), select(KeyCode.mouseLeft), diff --git a/core/src/mindustry/input/DesktopInput.java b/core/src/mindustry/input/DesktopInput.java index 6508e05476..de2abaac34 100644 --- a/core/src/mindustry/input/DesktopInput.java +++ b/core/src/mindustry/input/DesktopInput.java @@ -12,6 +12,8 @@ import arc.scene.ui.*; import arc.scene.ui.layout.*; import arc.util.*; import mindustry.*; +import mindustry.ai.types.*; +import mindustry.content.*; import mindustry.core.*; import mindustry.entities.units.*; import mindustry.game.EventType.*; @@ -114,6 +116,27 @@ public class DesktopInput extends InputHandler{ drawSelection(schemX, schemY, cursorX, cursorY, Vars.maxSchematicSize); } + for(Unit unit : selectedUnits){ + CommandAI ai = (CommandAI)unit.controller(); + //draw target line + if(ai.targetPos != null){ + Tmp.v1.set(ai.targetPos).sub(unit).setLength(unit.hitSize / 2f); + + Drawf.dashLine(Pal.accent, unit.x + Tmp.v1.x, unit.y + Tmp.v1.y, ai.targetPos.x, ai.targetPos.y); + } + + Drawf.square(unit.x, unit.y, unit.hitSize / 1.4f + 1f); + } + + //draw command overlay UI + if(commandMode){ + Unit sel = selectedCommandUnit(input.mouseWorldX(), input.mouseWorldY()); + + if(sel != null){ + Drawf.square(sel.x, sel.y, sel.hitSize / 1.4f + Mathf.absin(4f, 1f), selectedUnits.contains(sel) ? Pal.remove : Pal.accent); + } + } + Draw.reset(); } @@ -220,11 +243,19 @@ public class DesktopInput extends InputHandler{ Core.camera.position.x += Mathf.clamp((Core.input.mouseX() - Core.graphics.getWidth() / 2f) * panScale, -1, 1) * camSpeed; Core.camera.position.y += Mathf.clamp((Core.input.mouseY() - Core.graphics.getHeight() / 2f) * panScale, -1, 1) * camSpeed; } - } + commandMode = input.keyDown(Binding.commandMode) && !locked && state.rules.unitCommand && block == null; shouldShoot = !scene.hasMouse() && !locked; + //TODO should selected units be cleared out of command mode? + if(!commandMode){ + selectedUnits.clear(); + } + + //validate commanding units + selectedUnits.removeAll(u -> !u.isCommandable()); + if(!scene.hasMouse() && !locked){ if(Core.input.keyDown(Binding.control) && Core.input.keyTap(Binding.select)){ Unit on = selectedUnit(); @@ -503,6 +534,26 @@ public class DesktopInput extends InputHandler{ sreq = req; }else if(req != null && req.breaking){ deleting = true; + }else if(commandMode){ + Unit unit = selectedCommandUnit(input.mouseWorldX(), input.mouseWorldY()); + if(unit != null){ + if(selectedUnits.contains(unit)){ + selectedUnits.remove(unit); + }else{ + selectedUnits.add(unit); + } + }else if(selectedUnits.size > 0){ + //move to location - TODO right click instead? + + //TODO all this needs to be synced, done with packets, etc + Vec2 target = input.mouseWorld().cpy(); + + for(var sel : selectedUnits){ + ((CommandAI)sel.controller()).commandPosition(target); + } + + Fx.moveCommand.at(target); + } }else if(selected != null){ //only begin shooting if there's no cursor event if(!tryTapPlayer(Core.input.mouseWorld().x, Core.input.mouseWorld().y) && !tileTapped(selected.build) && !player.unit().activelyBuilding() && !droppingItem @@ -520,7 +571,7 @@ public class DesktopInput extends InputHandler{ }else if(Core.input.keyTap(Binding.deselect) && !selectRequests.isEmpty()){ selectRequests.clear(); lastSchematic = null; - }else if(Core.input.keyTap(Binding.break_block) && !Core.scene.hasMouse() && player.isBuilder()){ + }else if(Core.input.keyTap(Binding.break_block) && !Core.scene.hasMouse() && player.isBuilder() && !commandMode){ //is recalculated because setting the mode to breaking removes potential multiblock cursor offset deleting = false; mode = breaking; diff --git a/core/src/mindustry/input/InputHandler.java b/core/src/mindustry/input/InputHandler.java index 47783b10fc..1846074209 100644 --- a/core/src/mindustry/input/InputHandler.java +++ b/core/src/mindustry/input/InputHandler.java @@ -48,6 +48,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ /** Maximum line length. */ final static int maxLength = 100; final static Rect r1 = new Rect(), r2 = new Rect(); + final static Seq tmpUnits = new Seq<>(); public final OverlayFragment frag = new OverlayFragment(); @@ -71,6 +72,10 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ public Seq lineRequests = new Seq<>(); public Seq selectRequests = new Seq<>(); + //for RTS controls + public Seq selectedUnits = new Seq<>(); + public boolean commandMode = false; + private Seq plansOut = new Seq<>(BuildPlan.class); private QuadTree playerPlanTree = new QuadTree<>(new Rect()); @@ -1183,6 +1188,14 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ return null; } + public @Nullable Unit selectedCommandUnit(float x, float y){ + var tree = player.team().data().tree(); + tmpUnits.clear(); + float rad = 4f; + tree.intersect(x - rad/2f, y - rad/2f, rad, rad, tmpUnits); + return tmpUnits.min(u -> u.isCommandable(), u -> u.dst(x, y) - u.hitSize/2f); + } + public void remove(){ Core.input.removeProcessor(this); frag.remove(); @@ -1225,7 +1238,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ public boolean canShoot(){ return block == null && !onConfigurable() && !isDroppingItem() && !player.unit().activelyBuilding() && - !(player.unit() instanceof Mechc && player.unit().isFlying()) && !player.unit().mining(); + !(player.unit() instanceof Mechc && player.unit().isFlying()) && !player.unit().mining() && !commandMode; } public boolean onConfigurable(){ diff --git a/core/src/mindustry/maps/planet/ErekirPlanetGenerator.java b/core/src/mindustry/maps/planet/ErekirPlanetGenerator.java index 0a0ab6996a..9f774e942b 100644 --- a/core/src/mindustry/maps/planet/ErekirPlanetGenerator.java +++ b/core/src/mindustry/maps/planet/ErekirPlanetGenerator.java @@ -442,6 +442,7 @@ public class ErekirPlanetGenerator extends PlanetGenerator{ //it is very hot state.rules.attributes.set(Attribute.heat, 0.8f); state.rules.environment = sector.planet.defaultEnv; + state.rules.unitCommand = true; //TODO remove slag and arkycite around core. Schematics.placeLaunchLoadout(spawnX, spawnY); diff --git a/core/src/mindustry/type/UnitType.java b/core/src/mindustry/type/UnitType.java index b801fb9987..03237f17f6 100644 --- a/core/src/mindustry/type/UnitType.java +++ b/core/src/mindustry/type/UnitType.java @@ -49,6 +49,8 @@ public class UnitType extends UnlockableContent{ public Prov constructor; /** The default AI controller to assign on creation. */ public Prov defaultController = () -> !flying ? new GroundAI() : new FlyingAI(); + /** Function that chooses AI controller based on unit entity. */ + public Func unitBasedDefaultController = u -> defaultController.get(); /** Environmental flags that are *all* required for this unit to function. 0 = any environment */ public int envRequired = 0; @@ -208,10 +210,16 @@ public class UnitType extends UnlockableContent{ constructor = EntityMapping.map(this.name); } + /** @deprecated use the createController method instead */ + @Deprecated public UnitController createController(){ return defaultController.get(); } + public UnitController createController(Unit unit){ + return unitBasedDefaultController.get(unit); + } + public Unit create(Team team){ Unit unit = constructor.get(); unit.team = team; diff --git a/core/src/mindustry/type/unit/ErekirUnitType.java b/core/src/mindustry/type/unit/ErekirUnitType.java index 9f04228065..3cfb917d12 100644 --- a/core/src/mindustry/type/unit/ErekirUnitType.java +++ b/core/src/mindustry/type/unit/ErekirUnitType.java @@ -1,5 +1,6 @@ package mindustry.type.unit; +import mindustry.ai.types.*; import mindustry.graphics.*; import mindustry.type.*; import mindustry.world.meta.*; @@ -12,7 +13,9 @@ public class ErekirUnitType extends UnitType{ commandLimit = 0; outlineColor = Pal.darkOutline; envDisabled = Env.space; + //TODO necessary, or not? defaultAI = false; coreUnitDock = true; + unitBasedDefaultController = u -> !playerControllable || u.team.isAI() ? defaultController.get() : new CommandAI(); } }