Files
Mindustry/core/src/mindustry/entities/units/AIController.java
2024-11-30 23:59:29 -05:00

374 lines
11 KiB
Java

package mindustry.entities.units;
import arc.math.*;
import arc.math.geom.*;
import arc.util.*;
import mindustry.*;
import mindustry.entities.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.type.*;
import mindustry.world.*;
import mindustry.world.blocks.payloads.*;
import mindustry.world.meta.*;
import static mindustry.Vars.*;
public class AIController implements UnitController{
protected static final Vec2 vec = new Vec2();
protected static final float rotateBackTimer = 60f * 5f;
protected static final int timerTarget = 0, timerTarget2 = 1, timerTarget3 = 2, timerTarget4 = 3;
protected Unit unit;
protected Interval timer = new Interval(4);
protected AIController fallback;
protected float noTargetTime;
/** main target that is being faced */
protected Teamc target;
{
timer.reset(0, Mathf.random(40f));
timer.reset(1, Mathf.random(60f));
}
@Override
public void updateUnit(){
//use fallback AI when possible
if(useFallback() && (fallback != null || (fallback = fallback()) != null)){
if(fallback.unit != unit) fallback.unit(unit);
fallback.updateUnit();
return;
}
updateVisuals();
updateTargeting();
updateMovement();
}
/**
* @return whether controller state should not be reset after reading.
* Do not override unless you know exactly what you are doing.
* */
public boolean keepState(){
return false;
}
@Override
public void afterRead(Unit unit){
}
@Override
public boolean isLogicControllable(){
return true;
}
public void stopShooting(){
for(var mount : unit.mounts){
//ignore mount controllable stats too, they should not shoot either
mount.shoot = false;
}
}
@Nullable
public AIController fallback(){
return null;
}
public boolean useFallback(){
return false;
}
public void updateVisuals(){
if(unit.isFlying()){
if(unit.type.wobble) unit.wobble();
unit.lookAt(unit.prefRotation());
}
}
public void updateMovement(){
}
public void updateTargeting(){
if(unit.hasWeapons()){
updateWeapons();
}
}
/** For ground units: Looks at the target, or the movement position. Does not apply to non-omni units. */
public void faceTarget(){
if(unit.type.omniMovement || unit instanceof Mechc){
if(!Units.invalidateTarget(target, unit, unit.range()) && unit.type.faceTarget && unit.type.hasWeapons()){
unit.lookAt(Predict.intercept(unit, target, unit.type.weapons.first().bullet.speed));
}else if(unit.moving()){
unit.lookAt(unit.vel().angle());
}
}
}
public void faceMovement(){
if((unit.type.omniMovement || unit instanceof Mechc) && unit.moving()){
unit.lookAt(unit.vel().angle());
}
}
public boolean invalid(Teamc target){
return Units.invalidateTarget(target, unit.team, unit.x, unit.y);
}
public void pathfind(int pathTarget){
int costType = unit.pathType();
Tile tile = unit.tileOn();
if(tile == null) return;
Tile targetTile = pathfinder.getTargetTile(tile, pathfinder.getField(unit.team, costType, pathTarget));
if(tile == targetTile || !unit.canPass(targetTile.x, targetTile.y)) return;
unit.movePref(vec.trns(unit.angleTo(targetTile.worldx(), targetTile.worldy()), prefSpeed()));
}
public void updateWeapons(){
float rotation = unit.rotation - 90;
boolean ret = retarget();
if(ret){
target = findMainTarget(unit.x, unit.y, unit.range(), unit.type.targetAir, unit.type.targetGround);
}
noTargetTime += Time.delta;
if(invalid(target)){
target = null;
}else{
noTargetTime = 0f;
}
unit.isShooting = false;
for(var mount : unit.mounts){
Weapon weapon = mount.weapon;
float wrange = weapon.range();
//let uncontrollable weapons do their own thing
if(!weapon.controllable || weapon.noAttack) continue;
if(!weapon.aiControllable){
mount.rotate = false;
continue;
}
float mountX = unit.x + Angles.trnsx(rotation, weapon.x, weapon.y),
mountY = unit.y + Angles.trnsy(rotation, weapon.x, weapon.y);
if(unit.type.singleTarget){
mount.target = target;
}else{
if(ret){
mount.target = findTarget(mountX, mountY, wrange, weapon.bullet.collidesAir, weapon.bullet.collidesGround);
}
if(checkTarget(mount.target, mountX, mountY, wrange)){
mount.target = null;
}
}
boolean shoot = false;
if(mount.target != null){
shoot = mount.target.within(mountX, mountY, wrange + (mount.target instanceof Sized s ? s.hitSize()/2f : 0f)) && shouldShoot();
Vec2 to = Predict.intercept(unit, mount.target, weapon.bullet.speed);
mount.aimX = to.x;
mount.aimY = to.y;
}
mount.shoot = mount.rotate = shoot;
if(!shouldFire()){
mount.shoot = false;
}
unit.isShooting |= mount.shoot;
if(mount.target == null && !shoot && !Angles.within(mount.rotation, mount.weapon.baseRotation, 0.01f) && noTargetTime >= rotateBackTimer){
mount.rotate = true;
Tmp.v1.trns(unit.rotation + mount.weapon.baseRotation, 5f);
mount.aimX = mountX + Tmp.v1.x;
mount.aimY = mountY + Tmp.v1.y;
}
if(shoot){
unit.aimX = mount.aimX;
unit.aimY = mount.aimY;
}
}
}
public boolean checkTarget(Teamc target, float x, float y, float range){
return Units.invalidateTarget(target, unit.team, x, y, range);
}
/** @return whether the unit should actually fire bullets (as opposed to just targeting something) */
public boolean shouldFire(){
return true;
}
public boolean shouldShoot(){
return true;
}
public Teamc targetFlag(float x, float y, BlockFlag flag, boolean enemy){
if(unit.team == Team.derelict) return null;
return Geometry.findClosest(x, y, enemy ? indexer.getEnemy(unit.team, flag) : indexer.getFlagged(unit.team, flag));
}
public Teamc target(float x, float y, float range, boolean air, boolean ground){
return Units.closestTarget(unit.team, x, y, range, u -> u.checkTarget(air, ground), t -> ground && (unit.type.targetUnderBlocks || !t.block.underBullets));
}
public boolean retarget(){
return timer.get(timerTarget, target == null ? 40 : 90);
}
public Teamc findMainTarget(float x, float y, float range, boolean air, boolean ground){
return findTarget(x, y, range, air, ground);
}
public Teamc findTarget(float x, float y, float range, boolean air, boolean ground){
return target(x, y, range, air, ground);
}
public void commandTarget(Teamc moveTo){}
public void commandPosition(Vec2 pos){}
/** Called after this controller is assigned a unit. */
public void init(){
}
public @Nullable Tile getClosestSpawner(){
return Geometry.findClosest(unit.x, unit.y, Vars.spawner.getSpawns());
}
public void unloadPayloads(){
if(unit instanceof Payloadc pay && pay.hasPayload() && target instanceof Building && pay.payloads().peek() instanceof UnitPayload){
if(target.within(unit, Math.max(unit.type().range + 1f, 75f))){
pay.dropLastPayload();
}
}
}
public void circleAttack(float circleLength){
vec.set(target).sub(unit);
float ang = unit.angleTo(target);
float diff = Angles.angleDist(ang, unit.rotation());
if(diff > 70f && vec.len() < circleLength){
vec.setAngle(unit.vel().angle());
}else{
vec.setAngle(Angles.moveToward(unit.vel().angle(), vec.angle(), 6f));
}
vec.setLength(prefSpeed());
unit.movePref(vec);
}
public void circle(Position target, float circleLength){
circle(target, circleLength, prefSpeed());
}
public void circle(Position target, float circleLength, float speed){
if(target == null) return;
vec.set(target).sub(unit);
if(vec.len() < circleLength){
vec.rotate((circleLength - vec.len()) / circleLength * 180f);
}
vec.setLength(speed);
unit.movePref(vec);
}
public void moveTo(Position target, float circleLength){
moveTo(target, circleLength, 100f);
}
public void moveTo(Position target, float circleLength, float smooth){
moveTo(target, circleLength, smooth, unit.isFlying(), null);
}
public void moveTo(Position target, float circleLength, float smooth, boolean keepDistance, @Nullable Vec2 offset){
moveTo(target, circleLength, smooth, keepDistance, offset, false);
}
public void moveTo(Position target, float circleLength, float smooth, boolean keepDistance, @Nullable Vec2 offset, boolean arrive){
if(target == null) return;
float speed = prefSpeed();
vec.set(target).sub(unit);
float length = circleLength <= 0.001f ? 1f : Mathf.clamp((unit.dst(target) - circleLength) / smooth, -1f, 1f);
vec.setLength(speed * length);
if(arrive){
Tmp.v3.set(-unit.vel.x / unit.type.accel * 2f, -unit.vel.y / unit.type.accel * 2f).add((target.getX() - unit.x), (target.getY() - unit.y));
vec.add(Tmp.v3).limit(speed * length);
}
if(length < -0.5f){
if(keepDistance){
vec.rotate(180f);
}else{
vec.setZero();
}
}else if(length < 0){
vec.setZero();
}
if(offset != null){
vec.add(offset);
vec.setLength(speed * length);
}
//ignore invalid movement values
if(vec.isNaN() || vec.isInfinite() || vec.isZero()) return;
if(!unit.type.omniMovement && unit.type.rotateMoveFirst){
float angle = vec.angle();
unit.lookAt(angle);
if(Angles.within(unit.rotation, angle, 3f)){
unit.movePref(vec);
}
}else{
unit.movePref(vec);
}
}
public float prefSpeed(){
return unit.speed();
}
@Override
public void unit(Unit unit){
if(this.unit == unit) return;
this.unit = unit;
init();
}
@Override
public Unit unit(){
return unit;
}
}