001package com.fs.starfarer.api.impl.combat.dweller; 002 003import java.util.ArrayList; 004import java.util.Iterator; 005import java.util.List; 006 007import org.lwjgl.util.vector.Vector2f; 008 009import com.fs.starfarer.api.Global; 010import com.fs.starfarer.api.combat.CollisionClass; 011import com.fs.starfarer.api.combat.CombatEngineAPI; 012import com.fs.starfarer.api.combat.DamageType; 013import com.fs.starfarer.api.combat.ShipAIConfig; 014import com.fs.starfarer.api.combat.ShipAIPlugin; 015import com.fs.starfarer.api.combat.ShipAPI; 016import com.fs.starfarer.api.combat.ShipCommand; 017import com.fs.starfarer.api.combat.ShipwideAIFlags; 018import com.fs.starfarer.api.combat.WeaponGroupAPI; 019import com.fs.starfarer.api.util.IntervalUtil; 020import com.fs.starfarer.api.util.Misc; 021import com.fs.starfarer.api.util.WeightedRandomPicker; 022 023public class ShroudedVortexAI implements ShipAIPlugin { 024 025 public static class FlockingData { 026 public Vector2f loc; 027 public Vector2f vel; 028 public float minA; 029 public float maxA; 030 public float minR; 031 public float maxR; 032 public float repelAtAngleDist; 033 public float minC; 034 public float maxC; 035 public float attractWeight; 036 public float repelWeight; 037 public float cohesionWeight; 038 public float facing; 039 } 040 041 042 /** 043 * Loses hitpoints over time, when near zero, blows up. 044 */ 045 public static float HULL_FRACTION_LOST_PER_SECOND = 0.05f; 046 047 public static String VORTEX_FLOCKING = "vortex_flocking"; 048 public static float ATTRACTOR_RANGE_MAX = 2000f; 049 public static float COHESION_RANGE_MIN = 150f; 050 public static float COHESION_RANGE_MAX = 300f; 051 public static float REPEL_RANGE_MIN = 0f; 052 public static float REPEL_RANGE_MAX = 400f; 053 054 protected ShipwideAIFlags flags = new ShipwideAIFlags(); 055 protected ShipAPI ship; 056 protected boolean exploded = false; 057 protected Vector2f prevVel = null; 058 059 protected IntervalUtil updateInterval = new IntervalUtil(0.5f, 1.5f); 060 protected IntervalUtil headingInterval = new IntervalUtil(0.5f, 1.5f); 061 062 protected ShipAPI target = null; 063 protected float timeOnTarget = 0f; 064 protected float numCollisions = 0f; 065 066 protected List<FlockingData> flockingData = new ArrayList<>(); 067 protected float desiredHeading = 0f; 068 069 public ShroudedVortexAI(ShipAPI ship) { 070 this.ship = ship; 071 072 doInitialSetup(); 073 074 updateInterval.forceIntervalElapsed(); 075 } 076 077 protected void doInitialSetup() { 078 ship.addTag(VORTEX_FLOCKING); 079 } 080 081 protected void toggleOn(int groupNum) { 082 List<WeaponGroupAPI> groups = ship.getWeaponGroupsCopy(); 083 if (groups.size() <= groupNum) return; 084 groups.get(groupNum).toggleOn(); 085 } 086 protected void toggleOff(int groupNum) { 087 List<WeaponGroupAPI> groups = ship.getWeaponGroupsCopy(); 088 if (groups.size() <= groupNum) return; 089 groups.get(groupNum).toggleOff(); 090 } 091 092 093 @Override 094 public void advance(float amount) { 095 //if (true) return; 096 097 if (target != null) { 098 timeOnTarget += amount; 099 } 100 101 updateInterval.advance(amount); 102 if (updateInterval.intervalElapsed()) { 103 ShipAPI prev = target; 104 target = findTarget(); 105 if (prev != target) { 106 timeOnTarget = 0f; 107 } 108 109 updateFlockingData(); 110 } 111 112 headingInterval.advance(amount * 5f); 113 if (headingInterval.intervalElapsed()) { 114 computeDesiredHeading(); 115 } 116 117// String id = getClass().getSimpleName(); 118// if (timeOnTarget > 3f) { 119// ship.getMutableStats().getAcceleration().modifyMult(id, 0.24f); 120// ship.getMutableStats().getDeceleration().modifyMult(id, 0.24f); 121// } else { 122// ship.getMutableStats().getAcceleration().unmodifyMult(id); 123// ship.getMutableStats().getDeceleration().unmodifyMult(id); 124// } 125 126 if (prevVel != null) { 127 float delta = Vector2f.sub(prevVel, ship.getVelocity(), new Vector2f()).length(); 128 // likely collision, stop the ship to make it feel heavier 129 if (delta > ship.getMaxSpeedWithoutBoost() * 0.25f) { 130 ship.getVelocity().scale(0.1f); 131 numCollisions++; 132 } 133 } 134 prevVel = new Vector2f(ship.getVelocity()); 135 136 137 CombatEngineAPI engine = Global.getCombatEngine(); 138 139 DwellerShroud shroud = DwellerShroud.getShroudFor(ship); 140 if (shroud != null) { 141 ShipAPI sourceShip = (ShipAPI) shroud.custom1; 142 if (sourceShip != null) { 143 float dist = Misc.getDistance(ship.getLocation(), sourceShip.getLocation()); 144 if (dist > (ship.getCollisionRadius() + sourceShip.getCollisionRadius() * 0.75f)) { 145 ship.setCollisionClass(CollisionClass.SHIP); 146 } else { 147 ship.setCollisionClass(CollisionClass.FIGHTER); 148 } 149 } 150 } 151 152 153 float damage = ship.getMaxHitpoints() * HULL_FRACTION_LOST_PER_SECOND * (1f + numCollisions) * amount; 154 ship.setHitpoints(ship.getHitpoints() - damage); 155 if (ship.getHitpoints() <= 0f) { 156 // like other damage, this will trigger the explosion visuals/damage in ShroudedVortexShipCreator 157 engine.applyDamage(ship, ship.getLocation(), 10000f, DamageType.ENERGY, 0f, true, false, ship, false); 158 } 159 160 giveMovementCommands(); 161 } 162 163 protected void giveMovementCommands() { 164 CombatEngineAPI engine = Global.getCombatEngine(); 165 166 //ship.giveCommand(ShipCommand.DECELERATE, null, 0); 167 if (ship.hasTag(ShroudedVortexShipCreator.TAG_MIRRORED_VORTEX)) { 168 ship.giveCommand(ShipCommand.TURN_RIGHT, null, 0); 169 } else { 170 ship.giveCommand(ShipCommand.TURN_LEFT, null, 0); 171 } 172 173 float heading = Misc.getAngleInDegrees(ship.getVelocity()); 174 if (target != null) { 175 float speed = (ship.getVelocity().length() + ship.getMaxSpeed()) * 0.5f; 176 Vector2f point = engine.getAimPointWithLeadForAutofire(ship, 1f, target, speed); 177 heading = Misc.getAngleInDegrees(ship.getLocation(), point); 178 } 179 180 //engine.headInDirectionWithoutTurning(ship, heading, 10000); 181 182 engine.headInDirectionWithoutTurning(ship, desiredHeading, 10000); 183 } 184 185 186 @Override 187 public ShipwideAIFlags getAIFlags() { 188 return flags; 189 } 190 191 192 public ShipAPI findTarget() { 193 float range = 5000f; 194 float goodRange = ship.getHullLevel() / HULL_FRACTION_LOST_PER_SECOND * ship.getMaxSpeed() * 0.75f; 195 Vector2f from = ship.getLocation(); 196 197 CombatEngineAPI engine = Global.getCombatEngine(); 198 Iterator<Object> iter = engine.getShipGrid().getCheckIterator(from, 199 range * 2f, range * 2f); 200 int owner = ship.getOwner(); 201// ShipAPI best = null; 202// float maxScore = -100000f; 203 204 float currAngle = Misc.getAngleInDegrees(ship.getVelocity()); 205 206 WeightedRandomPicker<ShipAPI> good = new WeightedRandomPicker<>(); 207 WeightedRandomPicker<ShipAPI> lessGood = new WeightedRandomPicker<>(); 208 209 while (iter.hasNext()) { 210 Object o = iter.next(); 211 if (!(o instanceof ShipAPI)) continue; 212 213 ShipAPI other = (ShipAPI) o; 214 if (other == ship) continue; 215 if (other.getOwner() == owner) continue; 216 if (other.isHulk()) continue; 217 if (other.isPhased()) continue; 218 if (!engine.isAwareOf(owner, other)) 219 220 if (other.getCollisionClass() == CollisionClass.NONE) continue; 221 222 223 float dist = Misc.getDistance(from, other.getLocation()); 224 if (dist > range) continue; 225 226 float score = 1f; 227 if (other.isFrigate()) { 228 score = 11f; 229 } else if (other.isDestroyer()) { 230 score = 15f; 231 } else if (other.isCruiser() || other.isCapital()) { 232 score = 25f; 233 } 234 235 236 float angleToOther = Misc.getAngleInDegrees(ship.getLocation(), other.getLocation()); 237 float angleDiff = Misc.getAngleDiff(currAngle, angleToOther); 238 239 float f = angleDiff / 90f; 240 if (f > 1f) f = 1f; 241 242 float minus = 5f * dist / 5000f; 243 if (minus > 5f) minus = 3f; 244 245 score -= minus; 246 247 score -= f * 5f; 248 249 if (dist > goodRange) { 250 lessGood.add(other, score); 251 } else { 252 good.add(other, score); 253 } 254// if (dist > goodRange) { 255// score -= 100f; 256// } 257// 258// if (score > maxScore) { 259// maxScore = score; 260// best = other; 261// } 262 } 263 264 if (target != null) { 265 if (good.getItems().contains(target)) return target; 266 if (lessGood.getItems().contains(target) && good.isEmpty()) return target; 267 } 268 269 if (!good.isEmpty()) { 270 return good.pick(); 271 } 272 273 return lessGood.pick(); 274 } 275 276 277 278 279 protected void updateFlockingData() { 280 flockingData.clear(); 281 282 CombatEngineAPI engine = Global.getCombatEngine(); 283 284 int owner = ship.getOriginalOwner(); 285 Vector2f loc = ship.getLocation(); 286 float radius = ship.getCollisionRadius() * 1f; 287 288 if (target != null) { 289 float dist = Misc.getDistance(ship.getLocation(), target.getLocation()); 290 FlockingData data = new FlockingData(); 291 data.facing = target.getFacing(); 292 data.loc = target.getLocation(); 293 data.vel = target.getVelocity(); 294 data.attractWeight = 100f; 295 if (dist - ship.getCollisionRadius() - target.getCollisionRadius() < 500f) { 296 data.attractWeight *= 10f; 297 } 298 data.repelWeight = 0f; 299 data.minA = 0f; 300 data.maxA = 1000000f; 301 data.minR = 0f; 302 data.maxR = 0f; 303 data.repelAtAngleDist = 0f; 304 flockingData.add(data); 305 } 306 307 for (ShipAPI curr : engine.getShips()) { 308 if (curr == ship) continue; 309 if (curr.getOwner() != owner) continue; 310 if (curr.isHulk() || curr.getOwner() == 100) continue; 311 312 float currRadius = curr.getCollisionRadius(); 313 314 if (curr.hasTag(VORTEX_FLOCKING)) { 315 FlockingData data = new FlockingData(); 316 data.facing = Misc.getAngleInDegrees(curr.getVelocity()); 317 data.loc = curr.getLocation(); 318 data.vel = curr.getVelocity(); 319 data.repelWeight = 100f; 320 data.cohesionWeight = 1f; 321 data.attractWeight = 3f; 322 323 data.minA = 0f + radius + currRadius; 324 data.maxA = ATTRACTOR_RANGE_MAX + radius + currRadius; 325 326 data.minR = REPEL_RANGE_MIN + radius + currRadius; 327 data.maxR = REPEL_RANGE_MAX + radius + currRadius; 328 329 data.minC = COHESION_RANGE_MIN + radius + currRadius; 330 data.maxC = COHESION_RANGE_MAX + radius + currRadius; 331 332 flockingData.add(data); 333 } else if (!curr.isFighter()) { 334 FlockingData data = new FlockingData(); 335 data.facing = Misc.getAngleInDegrees(curr.getVelocity()); 336 data.loc = curr.getLocation(); 337 data.vel = curr.getVelocity(); 338 data.attractWeight = 0f; 339 data.cohesionWeight = 0f; 340 data.repelWeight = 100f; 341 342 data.minA = 0f; 343 data.maxA = 0f; 344 345 data.minR = REPEL_RANGE_MIN * 0.5f + radius + currRadius; 346 data.maxR = REPEL_RANGE_MAX * 0.5f + radius + currRadius; 347 348 data.minC = 0f; 349 data.maxC = 0f; 350 351 flockingData.add(data); 352 } 353 } 354 } 355 356 protected void computeDesiredHeading() { 357 358 Vector2f loc = ship.getLocation(); 359 Vector2f vel = ship.getVelocity(); 360 float facing = ship.getFacing(); 361 362 Vector2f total = new Vector2f(); 363 364 for (FlockingData curr : flockingData) { 365 float dist = Misc.getDistance(curr.loc, loc); 366 if (curr.maxR > 0 && dist < curr.maxR) { 367 float repelWeight = curr.repelWeight; 368 if (dist > curr.minR && curr.maxR > curr.minR) { 369 repelWeight = (dist - curr.minR) / (curr.maxR - curr.minR); 370 if (repelWeight > 1f) repelWeight = 1f; 371 repelWeight = 1f - repelWeight; 372 repelWeight *= curr.repelWeight; 373 } 374 375 Vector2f dir = Misc.getUnitVector(curr.loc, loc); 376 377 float distIntoRepel = curr.maxR - dist; 378 float repelAdjustmentAngle = 0f; 379 if (distIntoRepel < curr.repelAtAngleDist && curr.repelAtAngleDist > 0) { 380 float repelMult = (1f - distIntoRepel / curr.repelAtAngleDist); 381 repelAdjustmentAngle = 90f * repelMult; 382 repelWeight *= (1f - repelMult); 383 384 float repelAngle = Misc.getAngleInDegrees(dir); 385 float turnDir = Misc.getClosestTurnDirection(dir, vel); 386 repelAdjustmentAngle *= turnDir; 387 dir = Misc.getUnitVectorAtDegreeAngle(repelAngle + repelAdjustmentAngle); 388 } 389 390 dir.scale(repelWeight); 391 Vector2f.add(total, dir, total); 392 } 393 394 if (curr.maxA > 0 && dist < curr.maxA) { 395 float attractWeight = curr.attractWeight; 396 if (dist > curr.minA && curr.maxA > curr.minA) { 397 attractWeight = (dist - curr.minA) / (curr.maxA - curr.minA); 398 if (attractWeight > 1f) attractWeight = 1f; 399 attractWeight = 1f - attractWeight; 400 attractWeight *= curr.attractWeight; 401 } 402 403 Vector2f dir = Misc.getUnitVector(loc, curr.loc); 404 dir.scale(attractWeight); 405 Vector2f.add(total, dir, total); 406 } 407 408 if (curr.maxC > 0 && dist < curr.maxC) { 409 float cohesionWeight = curr.cohesionWeight; 410 if (dist > curr.minC && curr.maxC > curr.minC) { 411 cohesionWeight = (dist - curr.minC) / (curr.maxC - curr.minC); 412 if (cohesionWeight > 1f) cohesionWeight = 1f; 413 cohesionWeight = 1f - cohesionWeight; 414 cohesionWeight *= curr.cohesionWeight; 415 } 416 417 Vector2f dir = new Vector2f(curr.vel); 418 Misc.normalise(dir); 419 dir.scale(cohesionWeight); 420 Vector2f.add(total, dir, total); 421 } 422 } 423 424 if (total.length() <= 0) { 425 desiredHeading = ship.getFacing(); 426 } else { 427 desiredHeading = Misc.getAngleInDegrees(total); 428 } 429 } 430 431 432 public void setDoNotFireDelay(float amount) {} 433 public void forceCircumstanceEvaluation() {} 434 public boolean needsRefit() { return false; } 435 public void cancelCurrentManeuver() {} 436 public ShipAIConfig getConfig() { return null; } 437} 438 439 440 441 442 443 444 445 446 447 448 449 450