001package com.fs.starfarer.api.impl.combat.threat; 002 003import java.util.LinkedHashSet; 004import java.util.Set; 005 006import java.awt.Color; 007 008import org.lwjgl.util.vector.Vector2f; 009 010import com.fs.starfarer.api.Global; 011import com.fs.starfarer.api.combat.CombatEngineAPI; 012import com.fs.starfarer.api.combat.CombatEntityAPI; 013import com.fs.starfarer.api.combat.DamageType; 014import com.fs.starfarer.api.combat.DamagingProjectileAPI; 015import com.fs.starfarer.api.combat.EmpArcEntityAPI; 016import com.fs.starfarer.api.combat.EmpArcEntityAPI.EmpArcParams; 017import com.fs.starfarer.api.combat.EveryFrameWeaponEffectPlugin; 018import com.fs.starfarer.api.combat.GuidedMissileAI; 019import com.fs.starfarer.api.combat.MissileAPI; 020import com.fs.starfarer.api.combat.OnFireEffectPlugin; 021import com.fs.starfarer.api.combat.ShipAPI; 022import com.fs.starfarer.api.combat.WeaponAPI; 023import com.fs.starfarer.api.combat.WeaponAPI.AIHints; 024import com.fs.starfarer.api.impl.combat.threat.RoilingSwarmEffect.RoilingSwarmParams; 025import com.fs.starfarer.api.impl.combat.threat.RoilingSwarmEffect.SwarmMember; 026import com.fs.starfarer.api.util.Misc; 027import com.fs.starfarer.api.util.WeightedRandomPicker; 028 029public class BaseFragmentMissileEffect implements OnFireEffectPlugin, EveryFrameWeaponEffectPlugin, FragmentWeapon { 030 031 public static enum FragmentBehaviorOnImpact { 032 STOP_AND_FADE, 033 STOP_AND_FLASH, 034 KEEP_GOING, 035 } 036 037 protected DamagingProjectileAPI projectile; 038 protected WeaponAPI weapon; 039 protected CombatEngineAPI engine; 040 protected RoilingSwarmEffect sourceSwarm; 041 protected MissileAPI missile; 042 protected ShipAPI ship; 043 044 public BaseFragmentMissileEffect() { 045 } 046 047 048 @Override 049 public void advance(float amount, CombatEngineAPI engine, WeaponAPI weapon) { 050 ShipAPI ship = weapon.getShip(); 051 if (ship == null) return; 052 053 RoilingSwarmEffect swarm = RoilingSwarmEffect.getSwarmFor(ship); 054 int active = swarm == null ? 0 : swarm.getNumActiveMembers(); 055 int required = getNumFragmentsToFire(); 056 boolean disable = active < required; 057 weapon.setForceDisabled(disable); 058 059 showNoFragmentSwarmWarning(weapon, ship); 060 } 061 062 063 public void onFire(DamagingProjectileAPI projectile, WeaponAPI weapon, CombatEngineAPI engine) { 064 this.projectile = projectile; 065 this.weapon = weapon; 066 this.engine = engine; 067 068 if (!(projectile instanceof MissileAPI)) { 069 engine.removeEntity(projectile); 070 return; 071 } 072 missile = (MissileAPI) projectile; 073 if (missile.getSource() == null) { 074 engine.removeEntity(projectile); 075 return; 076 } 077 078 ship = missile.getSource(); 079 sourceSwarm = RoilingSwarmEffect.getSwarmFor(missile.getSource()); 080 if (sourceSwarm == null) { 081 engine.removeEntity(projectile); 082 return; 083 } 084 085 missile.setEmpResistance(getEMPResistance()); 086 087 SwarmMember fragment = pickPrimaryFragment(); 088 if (fragment == null) { 089 engine.removeEntity(projectile); 090 return; 091 } 092 093 if (missile.getWeapon() == null || !missile.getWeapon().hasAIHint(AIHints.RANGE_FROM_SHIP_RADIUS)) { 094 missile.setStart(new Vector2f(missile.getLocation())); 095 } 096 missile.getLocation().set(fragment.loc); 097 098 // picked fragment with velocity closest to that of missile, leave the missile's velocity as is 099 if (!shouldPickVelocityMatchingPrimaryFragment()) { 100 missile.getVelocity().set(fragment.vel); 101 boolean setFacing = false; 102 if (shouldMakeMissileFaceTargetOnSpawnIfAny()) { 103 if (missile.getAI() instanceof GuidedMissileAI) { 104 GuidedMissileAI ai = (GuidedMissileAI) missile.getAI(); 105 if (ai.getTarget() != null) { 106 missile.setFacing(Misc.getAngleInDegrees(fragment.loc, ai.getTarget().getLocation())); 107 setFacing = true; 108 } 109 } 110 } 111 if (!setFacing && fragment.vel.length() > 0.1f) { 112 missile.setFacing(Misc.getAngleInDegrees(fragment.vel)); 113 } 114 } 115 116 RoilingSwarmParams params = new RoilingSwarmParams(); 117 params.despawnSound = null; 118 params.maxSpeed = missile.getMaxSpeed() + 100f; 119 params.baseMembersToMaintain = 0; 120 params.removeMembersAboveMaintainLevel = false; 121 params.keepProxBasedScaleForAllMembers = true; 122 params.initialMembers = 0; 123 params.maxOffset = missile.getCollisionRadius() * 1.5f; 124 125 configureMissileSwarmParams(params); 126 127 // can't use data members inside the anon class since they'll change when it fires again 128 MissileAPI missile2 = missile; 129 FragmentBehaviorOnImpact behavior = getOtherFragmentBehaviorOnImpact(); 130 boolean explodeOnFizzling = explodeOnFizzling(); 131 String explosionSoundId = getExplosionSoundId(); 132 RoilingSwarmEffect missileSwarm = new RoilingSwarmEffect(missile2, params) { 133 boolean exploded = false; 134 Set<SwarmMember> stopped = new LinkedHashSet<>(); 135 int origMembers = 0; 136 boolean inited = false; 137 @Override 138 public void advance(float amount) { 139 super.advance(amount); 140 //if (true) return; 141 142 if (removeFragmentsWhenMissileLosesHitpoints() && !missile2.didDamage()) { 143 if (!inited) { 144 origMembers = members.size(); 145 inited = true; 146 } 147 if (origMembers > 0 && members.size() > 1 && missile2.getMaxHitpoints() > 0) { 148 float max = missile2.getMaxHitpoints(); 149 float hpPerMember = max / origMembers; 150 float hpLost = max - missile2.getHitpoints(); 151 int loseMembers = (int) (hpLost / hpPerMember); 152 int num = members.size(); 153 int alreadyLost = origMembers - num; 154 for (SwarmMember p : members) { 155 if (p.fader.isFadingOut()) { 156 alreadyLost++; 157 } 158 } 159 int lose = loseMembers - alreadyLost; 160 if (lose > 0) { 161 despawnMembers(lose, false); 162 } 163 } 164 } 165 166 fragment.loc.set(missile2.getLocation()); 167 fragment.vel.set(missile2.getVelocity()); 168 if (missile2.isFizzling() && engine.isMissileAlive(missile2)) { 169 fragment.fader.setBrightness(missile2.getCurrentBaseAlpha()); 170 } 171 if (missile2.didDamage()) { 172 if (behavior != FragmentBehaviorOnImpact.KEEP_GOING) { 173 CombatEntityAPI target = null; 174 if (missile2.getDamageTarget() instanceof CombatEntityAPI) { 175 target = (CombatEntityAPI) missile2.getDamageTarget(); 176 } 177 for (SwarmMember p : members) { 178 if (p == fragment || stopped.contains(p)) { 179 if (p == fragment) { 180 //p.fader.setDurationOut(0.5f); 181 } 182 continue; 183 } 184 boolean hit = false; 185 if (target != null && target.getExactBounds() != null) { 186 if (target instanceof ShipAPI) { 187 ShipAPI ship = (ShipAPI) target; 188 if (ship.getShield() != null) { 189 boolean inArc = ship.getShield().isWithinArc(p.loc); 190 if (inArc) { 191 hit = Misc.getDistance(p.loc, ship.getShieldCenterEvenIfNoShield()) < 192 ship.getShieldRadiusEvenIfNoShield(); 193 } 194 } 195 } 196 if (!hit) { 197 hit = target.isPointInBounds(p.loc); 198 } 199 } else { 200 Vector2f toP = Vector2f.sub(p.loc, fragment.loc, new Vector2f()); 201 hit = Vector2f.dot(toP, fragment.vel) > 0; 202 } 203 if (hit) { 204 p.vel.set(new Vector2f()); 205 if (behavior == FragmentBehaviorOnImpact.STOP_AND_FLASH) { 206 p.flash(); 207 } 208 reportFragmentHit(missile2, p, this, target); 209 stopped.add(p); 210 } 211 } 212 } 213 } 214 if (explodeOnFizzling && explosionSoundId != null) { 215 if ((missile2.isFizzling() || (missile2.getHitpoints() <= 0 && !missile2.didDamage())) && !exploded) { 216 exploded = true; 217 Global.getSoundPlayer().playSound(explosionSoundId, 1f, 1f, missile2.getLocation(), missile2.getVelocity()); 218 missile2.interruptContrail(); 219 engine.removeEntity(missile2); 220 missile2.explode(); 221 } 222 } 223 224 if ((missile2.isFizzling() || missile2.getHitpoints() <= 0) && !missile2.didDamage() && !exploded) { 225 params.minDespawnTime = 0.5f; 226 params.maxDespawnTime = 1f; 227 params.minFadeoutTime = 0.5f; 228 params.maxFadeoutTime = 1f; 229 setForceDespawn(true); 230 } 231 232 swarmAdvance(amount, missile2, this); 233 } 234 235// @Override 236// public int getNumMembersToMaintain() { 237// int base = params.baseMembersToMaintain; 238// float level = missile2.getHullLevel(); 239// int maintain = (int) Math.round(level * base); 240// if (maintain < 1) maintain = 1; 241// return maintain; 242// } 243 }; 244 245 sourceSwarm.removeMember(fragment); 246 missileSwarm.addMember(fragment); 247 fragment.rollOffset(missileSwarm.params, missile); 248 249 if (makePrimaryFragmentGlow()) { 250 if (fragment.flash != null) { 251 fragment.flash = null; 252 } 253 fragment.flashNext = null; 254 255 fragment.flash(); 256 fragment.flash.setBounceDown(false); 257 } 258 259 260 int transfer = getNumOtherMembersToTransfer(); 261 if (transfer > 0) { 262 sourceSwarm.transferMembersTo(missileSwarm, transfer, fragment.loc, getRangeForNearbyFragments()); 263 } 264 265 int add = getNumOtherMembersToAdd(); 266 if (addNewMembersIfNotEnoughToTransfer() && missileSwarm.members.size() - 1 < transfer) { 267 add += transfer - (missileSwarm.members.size() - 1); 268 } 269 if (add > 0) { 270 missileSwarm.addMembers(add); 271 } 272 273 swarmCreated(missile, missileSwarm, sourceSwarm); 274 275 float hpLoss = getHPLossPerTransferredMember(); 276 hpLoss *= 1 + transfer; 277 if (hpLoss > 0) { 278 ship.setHitpoints(ship.getHitpoints() - hpLoss); 279 // cause the swarm (or what's left of it) to despawn 280 if (ship.getHitpoints() <= 0) { 281 ship.setSpawnDebris(false); 282 engine.applyDamage(ship, ship.getLocation(), 100f, DamageType.ENERGY, 0f, true, false, missile, false); 283 } 284 } 285 286 if (withEMPArc()) { 287 spawnEMPArc(); 288 } 289 } 290 291 protected void swarmCreated(MissileAPI missile, RoilingSwarmEffect missileSwarm, RoilingSwarmEffect sourceSwarm) { 292 293 } 294 protected void reportFragmentHit(MissileAPI missile, SwarmMember p, RoilingSwarmEffect swarm, CombatEntityAPI target) { 295 296 } 297 298 protected float getHPLossPerTransferredMember() { 299 if (!ship.isFighter()) return 0f; 300 float hpLoss = ship.getMaxHitpoints() / (sourceSwarm.params.baseMembersToMaintain * 0.8f); 301 return hpLoss; 302 } 303 304 protected void configureMissileSwarmParams(RoilingSwarmParams params) { 305 params.flashFringeColor = new Color(255,50,50,255); 306 params.flashCoreColor = Color.white; 307 params.flashRadius = 60f; 308 params.flashCoreRadiusMult = 0.75f; 309 } 310 311 protected boolean shouldPickVelocityMatchingPrimaryFragment() { 312 if (missile.getAI() instanceof GuidedMissileAI) { 313 GuidedMissileAI ai = (GuidedMissileAI) missile.getAI(); 314 if (ai.getTarget() == null) { 315 return true; 316 } else { 317 return false; 318 } 319 } 320 return true; 321 } 322 323 protected boolean shouldMakeMissileFaceTargetOnSpawnIfAny() { 324 return false; 325 } 326 327 protected SwarmMember pickPrimaryFragment() { 328 if (shouldPickVelocityMatchingPrimaryFragment()) { 329 return pickVelocityMatchingFragmentWithinRange(getRangeFromSourceToPickFragments()); 330 } 331 return pickOuterFragmentWithinRange(getRangeFromSourceToPickFragments()); 332 } 333 334 protected SwarmMember pickOuterFragmentWithinRange(float range) { 335 SwarmMember best = null; 336 float maxDist = -Float.MAX_VALUE; 337 WeightedRandomPicker<SwarmMember> picker = sourceSwarm.getPicker(true, true); 338 while (!picker.isEmpty()) { 339 SwarmMember p = picker.pickAndRemove(); 340 float dist = Misc.getDistance(p.loc, sourceSwarm.getAttachedTo().getLocation()); 341 if (sourceSwarm.params.generateOffsetAroundAttachedEntityOval) { 342 //dist -= sourceSwarm.attachedTo.getCollisionRadius() * 0.75f; 343 dist -= Misc.getTargetingRadius(p.loc, sourceSwarm.attachedTo, false) + sourceSwarm.params.maxOffset - range * 0.5f; 344 } 345 if (dist > maxDist && dist < range) { 346 best = p; 347 maxDist = dist; 348 } 349 } 350 return best; 351 } 352 353 protected SwarmMember pickVelocityMatchingFragmentWithinRange(float range) { 354 Vector2f vel = missile.getVelocity(); 355 SwarmMember best = null; 356 float maxVelDiff = 0f; 357 WeightedRandomPicker<SwarmMember> picker = sourceSwarm.getPicker(true, true); 358 while (!picker.isEmpty()) { 359 SwarmMember p = picker.pickAndRemove(); 360 float dist = Misc.getDistance(p.loc, sourceSwarm.getAttachedTo().getLocation()); 361 if (sourceSwarm.params.generateOffsetAroundAttachedEntityOval) { 362 dist -= Misc.getTargetingRadius(p.loc, sourceSwarm.attachedTo, false) + sourceSwarm.params.maxOffset - range * 0.5f; 363 } 364 float velDiff = Misc.getDistance(p.vel, vel); 365 if (velDiff > maxVelDiff && dist < range) { 366 best = p; 367 maxVelDiff = dist; 368 } 369 } 370 return best; 371 } 372 373 protected SwarmMember pickOuterFragmentWithinRangeClosestTo(float range, Vector2f otherLoc) { 374 SwarmMember best = null; 375 float minDist = Float.MAX_VALUE; 376 WeightedRandomPicker<SwarmMember> picker = sourceSwarm.getPicker(true, true); 377 while (!picker.isEmpty()) { 378 SwarmMember p = picker.pickAndRemove(); 379 float dist = Misc.getDistance(p.loc, sourceSwarm.getAttachedTo().getLocation()); 380 if (sourceSwarm.params.generateOffsetAroundAttachedEntityOval) { 381 dist -= Misc.getTargetingRadius(p.loc, sourceSwarm.attachedTo, false) + sourceSwarm.params.maxOffset - range * 0.5f; 382 } 383 if (dist > range) continue; 384 dist = Misc.getDistance(p.loc, otherLoc); 385 if (dist < minDist) { 386 best = p; 387 minDist = dist; 388 } 389 } 390 return best; 391 } 392 393 protected boolean removeFragmentsWhenMissileLosesHitpoints() { 394 return true; 395 } 396 397 protected boolean makePrimaryFragmentGlow() { 398 return true; 399 } 400 401 protected float getRangeForNearbyFragments() { 402 return 75f; 403 } 404 protected float getRangeFromSourceToPickFragments() { 405 return 150f; 406 } 407 408 protected int getNumOtherMembersToTransfer() { 409 return 0; 410 } 411 protected boolean addNewMembersIfNotEnoughToTransfer() { 412 return true; 413 } 414 protected int getNumOtherMembersToAdd() { 415 return 0; 416 } 417 418 protected int getEMPResistance() { 419 return 0; 420 } 421 422 protected FragmentBehaviorOnImpact getOtherFragmentBehaviorOnImpact() { 423 return FragmentBehaviorOnImpact.STOP_AND_FLASH; 424 } 425 426 427 @Override 428 public int getNumFragmentsToFire() { 429 return 1 + getNumOtherMembersToTransfer(); 430 } 431 432 protected boolean explodeOnFizzling() { 433 return false; 434 } 435 436 protected String getExplosionSoundId() { 437 return null; 438 } 439 440 protected void swarmAdvance(float amount, MissileAPI missile, RoilingSwarmEffect swarm) { 441 442 } 443 444 445 protected boolean withEMPArc() { 446 return !ship.isFighter(); 447 } 448 449 protected Color getEMPFringeColor() { 450 Color c = weapon.getSpec().getGlowColor(); 451 //c = Misc.setAlpha(c, 127); 452 //c = Misc.scaleColorOnly(c, 0.75f); 453 return c; 454 } 455 456 protected Color getEMPCoreColor() { 457 return Color.white; 458 } 459 460 protected void spawnEMPArc() { 461 462 Vector2f from = weapon.getFirePoint(0); 463 464 EmpArcParams params = new EmpArcParams(); 465 params.segmentLengthMult = 4f; 466 467 params.glowSizeMult = 0.5f; 468 params.brightSpotFadeFraction = 0.33f; 469 params.brightSpotFullFraction = 0.5f; 470 params.movementDurMax = 0.2f; 471 params.flickerRateMult = 0.5f; 472 473 float dist = Misc.getDistance(from, missile.getLocation()); 474 float minBright = 100f; 475 if (dist * params.brightSpotFullFraction < minBright) { 476 params.brightSpotFullFraction = minBright / Math.max(minBright, dist); 477 } 478 479 float thickness = 20f; 480 481 EmpArcEntityAPI arc = engine.spawnEmpArcVisual(from, weapon.getShip(), 482 missile.getLocation(), 483 missile, 484 thickness, // thickness 485 getEMPFringeColor(), 486 getEMPCoreColor(), 487 params 488 ); 489 //arc.setCoreWidthOverride(thickness * coreWidthMult); 490 arc.setSingleFlickerMode(true); 491 arc.setUpdateFromOffsetEveryFrame(true); 492 //arc.setRenderGlowAtStart(false); 493 //arc.setFadedOutAtStart(true); 494 } 495 496} 497 498 499 500 501 502 503 504