package data.scripts.MissileAI;
import com.fs.starfarer.api.Global;
import com.fs.starfarer.api.combat.*;
import com.fs.starfarer.api.util.IntervalUtil;
import org.lazywizard.lazylib.MathUtils;
import org.lazywizard.lazylib.combat.AIUtils;
import org.lazywizard.lazylib.combat.CombatUtils;
import org.lwjgl.util.vector.Vector2f;
import java.awt.*;
import java.util.Iterator;
import java.util.List;
//import static jar.UtilityKit.LeadVector;
import static org.lwjgl.util.vector.Vector2f.add;
public class kitchenSINKai implements MissileAIPlugin
{
// Our missile object
private final MissileAPI missile;
// Our current target (can be null)
private CombatEntityAPI target;
//stuff that you can change (MAIN missile stage AI components)
private float straighttimer=(float)(10*Math.random()); //non targeting time
private IntervalUtil STimer = new IntervalUtil(.3f, .0f); //rate of countdown
private final double isEffectedbyflare=Math.random(); //flare stuff
private boolean flarelockout=false;
private float oftarget=30f*((float)(.5-Math.random())); //max initial off target (scatters missiles at launch)
private float baseoftarget=4f*((float)(.5-Math.random())); //min off target (makes missiles stay scattered)
//flarechasing
public static final String flare="flare";
public final float BaseIgnoreChance=.3f; //chance that flares cannot effect it by default
public final float LockCHance=.3f; //chance that future flares it encounters wont effect it
private IntervalUtil Timer = new IntervalUtil(.1f, .0f); //rate of countdown for off target
private final boolean StopNTurn=false; //when far off target missile will stop and turn to aim
//weaving behaviour
private final boolean Weave=false; //go in a zigzag path (improved over extingency script)
private float Weavesize=40f; //how far of course it will go, in degrees
private float WeaveFreq=40f; //how ofter it cycles (arbitary)
private float weavecount=0; //misc dont change
//scripts for HardTargeting/orbitrange
private final float MaxRange=850f; //max range when it starts to direct aim target
private final float MinRange=500f; //min range when it detonates
private final boolean HardTargeting=true;//will it turn to target once in range
//Heatseeker like behaviour (uses Min/Max range)
private final boolean Backtarget=true; //dose it circle around back of ship
private final float BackAng=45f; //min angle from rear
private final boolean Straferound=false; //strafe there or turn there
private boolean BTcutout=false; //shuts down the circling, don't touch
//DRONE script (core behaviour script)
private final boolean isDrone=false; //makes missile bob in and out of MinRange and maintain that distance
private final boolean Orbit=false; //will the drone strafe around target?
private double ODir=Math.random(); //direction of strafe
private IntervalUtil OTimer = new IntervalUtil(3f, .01f); //delay between strafe switch
//used in Backtarget as well
private final float MinOrbit=.9f; //how close will the drone move in
private final float MaxOrbit=1.2f; //how far will the drone move away
private boolean Close=true; //thruster bool, don't touch
//weapons
//controls firing, everything below needs this on
private final boolean canFire=false; //is shooting enabled
//////////////////////////////////////////////////////////////////////////////////////////////////////////
//weaponlist
//nasty stuff drone uses as weapons
//replace with projectile cannon like "chaingun" or "vulcan" from vanilla,
// or your own weapon(no beams)(Ballistic_as_beam are fine)
private static String [] Guns = {
"DUMMY1",
"DUMMY2",
"chaingun",
"vulcan",
};
//and missiles
private static String [] Missiles = {
"Linghning", //special, shoots lightning bolts
"harpoon_single",
"heatseeker",
};
//////////////////////////////////////////////////////////////////////////////////////////////////////
//weaponpicker
private float MislChance=.8f; //chance of missiles (1-MislChance)*100=%shots missile
private int MislVolley=1; //number of missiles per
private int ProjVolley=4; //number of Projectiles per
private boolean Missil=(Math.random() > MislChance); //is it firing a missile
private String Projectile = (Missil? Missiles:Guns) //picks the weapon
[(int) ((Missil? Missiles:Guns).length * Math.random())];
//MIRV/Flechet script (core firing script of a drone missile)
//dummy weapon supports missiles and projectiles
//suggested projectile stats, range:1, speed:1
private final float PArk=25; //ark projectiles fly in
private final int PNum=1; //shots per volley
private int PVolleys=Missil ? MislVolley:ProjVolley; //volleys/burst fire for drone
private int VCount=0; //counter
private final float Lifetime=1f; //how long the projectile being used can stay alive (IMPORTANT)
//this is a value you must enter it is equal to range/ProjSpeed and works as a multiplier to make
// firing velocity not overshoot the target (due to long life)
private final float Leadtime=1f; //fiddle with this if your projectiles are missing the target
//determines how far ahead of the target your weapon aims
private IntervalUtil VTimer = new IntervalUtil(.15f, .01f); //rate of fire
private final boolean IsFlechet=true;//are the shots fired by missile aimed or follow missile facing?
//if IsFlechet=false missile will fire shots at the target ignoring its direction of travel
private boolean Loaded=true; //fireing bool
private boolean DespawnEmpty=false; //should it de-spawn the missile once it has run out of ammo
private boolean SpawnEmpty=false; //to spawn an empty type subprojectile(casing)
private boolean canReload=true; //can reload after a burst
private IntervalUtil BTimer = new IntervalUtil(1.8f, .01f); //delay between each burst
public static void emptyspawn(CombatEngineAPI engine, MissileAPI missile)
{
engine.spawnProjectile(missile.getSource(),null,//who made you
"DUMMY2", //empty missile,
missile.getLocation(), //Vector2f firing origin point
missile.getFacing(), //float angle of fire
missile.getVelocity()); //Vector2f firing velocity)
}
//end of stuff you can change
//////////////////////////////////////////////////////////////////////////////////////////////////////////
public static float searchrange = 500; //500 range default
//public static float Ark = 30; //60deg ark default
boolean newlaunch=true; //only do mouse target once on a newly launched missile
static float MWRange = 9999f; //hack to make missile ignore null weapon
public kitchenSINKai(MissileAPI missile)
{
this.missile = missile;
MWRange = missile.getWeapon() != null ? missile.getWeapon().getRange() : 9999f;
searchrange=MWRange;//based on weapon
//Ark=5+missile.getWeapon().getArc()/4; //based on weapon
//missile.setCollisionClass(CollisionClass.FIGHTER);//hack reclasify as fighter
// Support for 'fire at target by clicking on them' behavior
if (newlaunch=true)
{
newlaunch=false;
//get targets near mouse
Vector2f MouseT = missile.getSource().getMouseTarget();
if (MathUtils.getDistance(missile,MouseT)<searchrange)
{
List directTargets = CombatUtils.getShipsWithinRange(
MouseT, 100f, true);
if (!directTargets.isEmpty())
{
ShipAPI tmp;
for (Iterator iter = directTargets.iterator(); iter.hasNext();)
{
//filter out friendlies
tmp = (ShipAPI) iter.next();
if (tmp.getOwner() != missile.getSource().getOwner())
{
target = tmp;
break;
}
}
}
}
}
// Otherwise, use default targeting AI
if (target == null)
{
target = findBestTarget(missile);
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
//flare finder
public static MissileAPI checkforflare(MissileAPI missile)
{
MissileAPI retv = null; //if no flares return null
CombatEngineAPI engine = Global.getCombatEngine();
List nearbymissiles = engine.getMissiles(); //(target.getCollisionRadius()*10)
if (!nearbymissiles.isEmpty())
{
MissileAPI tmp;
for (Iterator iter = nearbymissiles.iterator(); iter.hasNext();)
{
tmp = (MissileAPI) iter.next();
if ((tmp.getProjectileSpecId()).startsWith(flare) //is it a flare
&& MathUtils.getDistance(tmp,missile)<300 //is it near your missile
&& missile.getOwner()!=tmp.getOwner() //is it en enemy flare
&& Math.random()>.5) //chance of failure prevents all missiles
{ //going after one flare at the same time
//tmp= (MissileAPI) nearbymissiles.get(0); //testing code
retv = tmp; //return flare
break; //stop searching you found your flare
}
}
}
return retv;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
/* turned off for this AI
//search for targets in ark in front
public static MissileAPI findBestTarget(MissileAPI missile)
{
//find targets in ark in front of missile
ArrayList targets = getEnemyMissilesInArk(missile, Ark, searchrange, true);
if (!targets.isEmpty())
{ //pick random missile in list //replace with .get(0) for nearest
return (MissileAPI) targets.get((int)(targets.size()*Math.random()));
}
else return null;
}
*/
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
//get ship target or search for closest enemy
public static ShipAPI findBestTarget(MissileAPI missile)
{
ShipAPI source = missile.getSource(); //who launched you?
if (source != null && source.getShipTarget() != null //check for nulls
&& !source.getShipTarget().isHulk() //check if its alive
&& MathUtils.getDistance(source.getShipTarget(), source) //get range to target
<MWRange) //check if its in range
{
return source.getShipTarget();
}
return AIUtils.getNearestEnemy(missile);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//main missile AI
@Override
public void advance(float amount)
{
//timer
Timer.advance(amount);
STimer.advance(amount);
VTimer.advance(amount);
OTimer.advance(amount);
BTimer.advance(amount);
weavecount++;
if (weavecount>4000){weavecount=0;} //arbitary reset
////////////////////////////////////////////////////////////////////////////////////////////////////////////
//go straight for x after launch
if (straighttimer>0)
{
missile.giveCommand(ShipCommand.ACCELERATE);
if (STimer.intervalElapsed())
{straighttimer--;}
return;
}
else
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////
// Apparently commands still work while fizzling
if (missile.isFading() || missile.isFizzling())
{
return;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
//find a target
if (target == null)
{
target = findBestTarget(missile);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
//if missile is susceptible to flare
if (isEffectedbyflare>BaseIgnoreChance //can a flare effect it
&& !flarelockout //has it already been set to ignore flares
&& Timer.intervalElapsed()) //check every .10 sec
{
MissileAPI flaretarget = checkforflare(missile);
if (flaretarget!=null)
{
target=flaretarget; //set flare as target
if (LockCHance<(float)Math.random())flarelockout=true; //ignore future flares
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
//pathing ai
if (target!=null) //if you have a target
{
//reduction off target/gradual aiming
if (Timer.intervalElapsed())
{
oftarget=(oftarget>0 ? oftarget-1 : oftarget+1); //reduce off target counter;
}
//////////////////////////////////////////////////////////////////////////////////////////////
//check if scatter code needed
float targetcolision = target.getCollisionRadius(); //get colision radi
if (MathUtils.getDistance(missile, target)<75+(targetcolision))
{ //cutoff for baseoftarget behaviour
baseoftarget=0; //aim variable if you got in close enough is 0 (ie go hit the enemy target)
}
///////////////////////////////////////////////////////////////////////////////////////////////
//how much off target will the missiles be
float oftargetby = (0 + (oftarget + (baseoftarget * targetcolision / 75)));
//////////////////////////////////////////////////////////////////////////////////////////
//aming
//1step calculations of various stats
Vector2f MVel = missile.getVelocity();
Vector2f MLoc = missile.getLocation();
Vector2f TLoc = target.getLocation();
Vector2f TVel = target.getVelocity();
//float MSpeed = (float)Math.sqrt(MVel.lengthSquared());
//float TSpeed = (float)Math.sqrt(TVel.lengthSquared());
float MFace = missile.getFacing();
float TFace = target.getFacing();
double TfaceRad = Math.toRadians(TFace);
float TCol = target.getCollisionRadius();
float sx = (float) (TCol*.5*Math.cos(TfaceRad));
float sy = (float) (TCol*.5*Math.sin(TfaceRad));
Vector2f TNose = new Vector2f(sx, sy);
float RangeToTarget = MathUtils.getDistance(TLoc, MLoc);
//float TimeToTarget = RangeToTarget/MSpeed; //how long till you hit
//testing InterceptPoint
Vector2f Lvec = LeadVector(TLoc, TVel, MLoc, MVel);
//////////////////////////////////////////////////////////////////////////////////////////
//override when in range
if (HardTargeting && RangeToTarget<MaxRange)
{
Lvec= multV2f(TVel, Leadtime);
baseoftarget=0;
oftarget=0;
oftargetby=0;
}
//////////////////////////////////////////////////////////////////////////////////////////
//lead target
Vector2f TLead = add(TLoc, //target location+
Lvec, //target speed*time to get to target+
null); // if its too long just assume 3 seconds is long enough to course correct
//aim at nose (can be changed to part targeting with a few tweaks)
Vector2f TNoseLead = add(TLead, TNose, null);//aim at the nose of the target(optional)
//main aiming (to find angle you are off target by)
float AngleToEnemy = MathUtils.getAngle(MLoc, TNoseLead);
float AtTarget = getAngleDifference( //how far off target you are
MFace, AngleToEnemy); //where missile pointing, where target is in relation
//for backtargeting
if (Backtarget && RangeToTarget<MaxRange)
{
float AngFromTtoM = MathUtils.getAngle(TLoc, MLoc); //get angle from target
float ToMissile = getAngleDifference(TFace, AngFromTtoM); //to missile
if (Math.abs(ToMissile)>180-BackAng) //check if you are behind the target
{BTcutout=true;}
else if (isDrone) //override for drones, re enables the AI
{BTcutout=false;}
if (!Straferound && !BTcutout)
{
AtTarget=AtTarget-(ToMissile>0? 90:-90);
}
if (Straferound && !BTcutout)
{
missile.giveCommand(ToMissile < 0
? ShipCommand.STRAFE_LEFT : ShipCommand.STRAFE_RIGHT);
}
}
//makes the missile weave
if (Weave)
{AtTarget=AtTarget+(float)(Math.sin(weavecount/WeaveFreq)*Weavesize);}
float AbsAngD = Math.abs(AtTarget-oftargetby);
//////////////////////////////////////////////////////////////////////////////////////////////
//point towards target
if (AbsAngD > 0.5)
{ //makes missile fly off target
missile.giveCommand(AtTarget > oftargetby
? ShipCommand.TURN_LEFT : ShipCommand.TURN_RIGHT);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
//correct missile velocity vector to be the same as missile facing
if (AbsAngD < 5)
{
//course correct for missile velocity vector
float MFlightAng = MathUtils.getAngle(new Vector2f(0, 0), MVel);
float MFlightCC = getAngleDifference(MFace, MFlightAng);
if (Math.abs(MFlightCC)>20)
{
missile.giveCommand(MFlightCC < 0
? ShipCommand.STRAFE_LEFT : ShipCommand.STRAFE_RIGHT);
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////
//stop turning once you are on target (way of the hack, ignores missile limitations)
if (AbsAngD < 0.4)
{
missile.setAngularVelocity(0);
}
///////////////////////////////////////////////////////////////////////////////////////////////
if((!isDrone && !Straferound) //not a drone or strafingBT
|| (isDrone && RangeToTarget>MaxRange) //is out or orbital range
|| (Backtarget && RangeToTarget>MaxRange && !BTcutout)
|| BTcutout) //is BTcutout
{
//acceleration code (stopNturn compatible)
if (StopNTurn)
{
if (AbsAngD < 40)
{
missile.giveCommand(ShipCommand.ACCELERATE);
}
if (MVel.lengthSquared()<TVel.lengthSquared())
{
missile.giveCommand(ShipCommand.ACCELERATE);
}
if (AbsAngD > 120) //if you missed stop and turn around
{
missile.giveCommand(ShipCommand.DECELERATE);
}
}
else{missile.giveCommand(ShipCommand.ACCELERATE);}
}
/////////////////////////////////////////////////////////////////////////////////////////////////
else //move back and forth, if its a drone
{
if (RangeToTarget<MinRange*MinOrbit){Close=false;} //boolean to achive back and forth motion
if (RangeToTarget>MinRange*MaxOrbit){Close=true;}
if (Close) {missile.giveCommand(ShipCommand.ACCELERATE);}
else {missile.giveCommand(ShipCommand.ACCELERATE_BACKWARDS);}
/////////////////////////////////////////////////////////////////////////////////////////////////
//orbit target
if (Orbit)
{
if (OTimer.intervalElapsed())
{
ODir=Math.random();
}
missile.giveCommand(ODir < .5
? ShipCommand.STRAFE_LEFT : ShipCommand.STRAFE_RIGHT);
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////
//testcode spawns particle at aim points
CombatEngineAPI engine = Global.getCombatEngine(); //engine
//engine.addSmoothParticle(TLead, new Vector2f(0,0), 5, 1, 1, new Color(255,10,15,255));
//engine.addSmoothParticle(TNoseLead, new Vector2f(0,0), 5, 1, 1, new Color(93, 255, 40,255));
//engine.addSmoothParticle(MLoc, multV2f(MVel, 4), 5, .5f, 1, new Color(248, 244, 255,255));
//engine.addSmoothParticle(MLoc, new Vector2f(0,0), 5, .5f, 1, new Color(2, 255, 250,255));
/////////////////////////////////////////////////////////////////////////////////////////////////
if (canFire) //overriding controll for firing
{
//reload
if (canReload && !Loaded && BTimer.intervalElapsed())
{
Loaded=true;
VCount=0;
{
//weaponpicker
Missil=(Math.random() > MislChance); //is it firing a missile
Projectile = (Missil? Missiles:Guns) //picks the weapon
[(int) ((Missil? Missiles:Guns).length * Math.random())];
PVolleys=Missil ? MislVolley:ProjVolley;
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////
if (Loaded && VTimer.intervalElapsed() && RangeToTarget<MinRange && VCount<PVolleys)
{
VCount++;
{
if (Projectile.equals("Linghning")) //cause lightningbolts!!
{
engine.spawnEmpArc(missile.getSource(), MLoc, target, target,
DamageType.ENERGY,
50f,
2000,
10000f, // max range
"tachyon_lance_emp_impact",
20f, // thickness
new Color(255,10,15,255),
new Color(255,255,255,255)
);
}
else
{
//spawn projectiles
int counts=0; //how many projectiles?
do {
float angRAND= (float) ((PArk*(.5-Math.random()))); //angle of spread
float speedRAND= (float) (((MaxRange-MinRange)/Lifetime)*Math.random());//variance of speed
float PSpeed = (MinRange/Lifetime)+speedRAND; //speed of bullets launched
float PAng = (IsFlechet ? missile.getFacing() //direction of fire
:missile.getFacing()+AtTarget)-angRAND;
float x = (float) (PSpeed*Math.cos(Math.toRadians(PAng)));
float y = (float) (PSpeed*Math.sin(Math.toRadians(PAng)));
Vector2f PVec = new Vector2f(x,y); //convert all that into a vector
Vector2f PMVec = add(MVel, PVec, null); //true Pvec
//for missile weapon offset
float xa = (float) (11*Math.cos(Math.toRadians(missile.getFacing()-95)));
float ya = (float) (11*Math.sin(Math.toRadians(missile.getFacing()-95)));
Vector2f MoffVec = new Vector2f(xa,ya);
//for projectile weapon ofset
float xb = (float) (25*Math.cos(Math.toRadians(missile.getFacing()+45)));
float yb = (float) (25*Math.sin(Math.toRadians(missile.getFacing()+45)));
Vector2f PoffVec = new Vector2f(xb,yb);
Vector2f Lpoint = add(MLoc, Missil ? MoffVec : PoffVec, null);
engine.spawnProjectile(
missile.getSource(),
null, //who made you
Projectile, //spawn the projectile
Lpoint, //Vector2f firing origin point
PAng, //float angle of fire
PMVec); //Vector2f firing velocity
/////////////////////////////////////////////////////////////////////////////////////
counts++;
////////////////////////////////////////////////////////////////////////////////
//optional explosion/particle glows
//engine.addHitParticle(Lpoint, PMVec,
// 5f,1f,1f,new Color(112, 152, 137,255)); //size, alpha, duration, color
engine.spawnExplosion(Lpoint, PMVec,
new Color(255, 86, 13,255), 15f, .1f); //color,size,duration(max)
//////////////////////////////////////////////////////////////////////////////////
}while (counts<PNum);
}
}
//////////////////////////////////////////////////////////////////////////////////
//main explosion flash
//engine.addHitParticle(MLoc, missile.getVelocity(),10,1f,1f,new Color(255, 12, 25,255));
}
//////////////////////////////////////////////////////////////////////////////////
if (VCount>=PVolleys)
{
Loaded=false;
if(DespawnEmpty)
{
if (SpawnEmpty) {emptyspawn(engine,missile);}
engine.removeEntity(missile);
}
}
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//stop if you have no target
if (target==null && straighttimer<1)
{missile.giveCommand(ShipCommand.DECELERATE);}
///////////////////////////////////////////////////////////////////////////////////////////////////////////
//clear target if target is gone
if (target == null // unset
|| ( missile.getOwner() == target.getOwner() ) // friendly
|| !Global.getCombatEngine().isEntityInPlay(target) // completely removed
|| ( target instanceof ShipAPI && ((ShipAPI)target).isHulk() ))//dead
{
target = findBestTarget(missile);
return;
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Will be in next LazyLib version
public static float getAngleDifference(float angle1, float angle2)
{
float distance = (angle2 - angle1) + 180f;
distance = (distance / 360.0f);
distance = ((distance - (float) Math.floor(distance)) * 360f) - 180f;
return distance;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
//multiply vectors //own code
public static Vector2f multV2f(Vector2f Vector1, float Multiplier)
{
float v1x = Vector1.getX()*Multiplier;
float v1y = Vector1.getY()*Multiplier;
Vector2f v1end = new Vector2f(v1x, v1y);
return v1end;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//find intercept between a missile and a target
//adapted from code by Jens Seiler
//http://jaran.de/goodbits/2011/07/17/calculating-an-intercept-course-to-a-target-with-constant-direction-and-velocity-in-a-2-dimensional-plane/
//have fun if you want to actually understand what going on
//basicaly gets where T will be by the time M gets to it
public static Vector2f LeadVector(Vector2f TLoc, Vector2f TVel, Vector2f MLoc, Vector2f MVel)
{
//get missiles speed
float MSpeed = (float)Math.sqrt(MVel.lengthSquared());
//separate out the vectors
float Tx = TLoc.getX();
float Ty = TLoc.getY();
float Mx = MLoc.getX();
float My = MLoc.getY();
float TVx = TVel.getX();
float TVy = TVel.getY();
float MVx = MVel.getX();
float MVy = MVel.getY();
//subtract position vectors
float x1 = Tx - Mx;
float y1 = Ty - My;
//quadratic fun
float h1 = TVx*TVx + TVy*TVy - MSpeed*MSpeed;
if (h1==0)
{
h1= (float) .0001;
}
float minusMHalf = -(x1*TVx + y1*TVy)/h1; // h2/h1
float discriminant = minusMHalf * minusMHalf - (x1*x1 + y1*y1)/h1; // (h2/h1)^2-h3/h1 (ie D)
//can they intersect?
if (discriminant < 0)
{
return TLoc;
}
double root = Math.sqrt(discriminant);
double t1 = minusMHalf + root;
double t2 = minusMHalf - root;
double tMin = Math.min(t1, t2);
double tMax = Math.max(t1, t2);
//which of the 2 is smaller
double time = tMin > 0 ? tMin : tMax;
//can return -ve time (this is less then useful)
if (time < 0)
{
return TLoc;
}
//calculate vector
return new Vector2f((float)(time * TVx), (float)(time * TVy));
}
}