001package com.fs.starfarer.api.impl.combat; 002 003import java.util.ArrayList; 004import java.util.HashMap; 005import java.util.Iterator; 006import java.util.List; 007import java.util.Map; 008 009import java.awt.Color; 010 011import org.lwjgl.util.vector.Vector2f; 012 013import com.fs.starfarer.api.Global; 014import com.fs.starfarer.api.combat.BaseEveryFrameCombatPlugin; 015import com.fs.starfarer.api.combat.CollisionClass; 016import com.fs.starfarer.api.combat.CombatEngineAPI; 017import com.fs.starfarer.api.combat.CombatEngineLayers; 018import com.fs.starfarer.api.combat.CombatEntityAPI; 019import com.fs.starfarer.api.combat.DamageType; 020import com.fs.starfarer.api.combat.EmpArcEntityAPI; 021import com.fs.starfarer.api.combat.EmpArcEntityAPI.EmpArcParams; 022import com.fs.starfarer.api.combat.EveryFrameCombatPlugin; 023import com.fs.starfarer.api.combat.MissileAPI; 024import com.fs.starfarer.api.combat.MutableShipStatsAPI; 025import com.fs.starfarer.api.combat.ShipAPI; 026import com.fs.starfarer.api.combat.ShipSystemAPI; 027import com.fs.starfarer.api.combat.ShipSystemAPI.SystemState; 028import com.fs.starfarer.api.combat.WeaponAPI.WeaponSize; 029import com.fs.starfarer.api.impl.campaign.ids.HullMods; 030import com.fs.starfarer.api.input.InputEventAPI; 031import com.fs.starfarer.api.loading.WeaponSlotAPI; 032import com.fs.starfarer.api.util.IntervalUtil; 033import com.fs.starfarer.api.util.Misc; 034import com.fs.starfarer.api.util.WeightedRandomPicker; 035 036public class MoteControlScript extends BaseShipSystemScript { 037 038 protected static float MAX_ATTRACTOR_RANGE = 3000f; 039 public static float MAX_DIST_FROM_SOURCE_TO_ENGAGE_AS_PD = 2000f; 040 public static float MAX_DIST_FROM_ATTRACTOR_TO_ENGAGE_AS_PD = 1000f; 041 042 public static int MAX_MOTES = 30; 043 public static int MAX_MOTES_HF = 50; 044 045 public static float ANTI_FIGHTER_DAMAGE = 200; 046 public static float ANTI_FIGHTER_DAMAGE_HF = 1000; 047 048 public static float ATTRACTOR_DURATION_LOCK = 20f; 049 public static float ATTRACTOR_DURATION = 10f; 050 051 //public static Color JITTER_COLOR = new Color(100,155,255,175); 052 053 054// static { 055// // .wpn 056// Color muzzleFlashColor = new Color(100,165,255,25); 057// 058// // .proj 059// Color hitGlowColor = new Color(100,100,255,255); 060// Color engineGlowColor = new Color(100,165,255,255); 061// Color contrailColor = new Color(100,165,255,25); 062// 063// //MoteControlScript 064// Color jitterColor = new Color(100,165,255,175); 065// Color empArcColor = new Color(100,165,255,255); 066// 067// // on hit effect 068// Color onHitEmpColor = new Color(100,165,255,255); 069// } 070 071 public static class MoteData { 072 public Color jitterColor; 073 public Color empColor; 074 075 public int maxMotes; 076 077 public float antiFighterDamage; 078 public String impactSound; 079 public String loopSound; 080 } 081 082 public static Map<String, MoteData> MOTE_DATA = new HashMap<String, MoteData>(); 083 084 public static String MOTELAUNCHER = "motelauncher"; 085 public static String MOTELAUNCHER_HF = "motelauncher_hf"; 086 087 static { 088 MoteData normal = new MoteData(); 089 normal.jitterColor = new Color(100,165,255,175); 090 normal.empColor = new Color(100,165,255,255); 091 normal.maxMotes = MAX_MOTES; 092 normal.antiFighterDamage = ANTI_FIGHTER_DAMAGE; 093 normal.impactSound = "mote_attractor_impact_normal"; 094 normal.loopSound = "mote_attractor_loop"; 095 096 MOTE_DATA.put(MOTELAUNCHER, normal); 097 098 MoteData hf = new MoteData(); 099 hf.jitterColor = new Color(255,100,255,175); 100 hf.empColor = new Color(255,100,255,255); 101 hf.maxMotes = MAX_MOTES_HF; 102 hf.antiFighterDamage = ANTI_FIGHTER_DAMAGE_HF; 103 hf.impactSound = "mote_attractor_impact_damage"; 104 hf.loopSound = "mote_attractor_loop_dark"; 105 106 MOTE_DATA.put(MOTELAUNCHER_HF, hf); 107 } 108 109 public static boolean isHighFrequency(ShipAPI ship) { 110 //if (true) return true; 111 return ship != null && ship.getVariant().hasHullMod(HullMods.HIGH_FREQUENCY_ATTRACTOR); 112 } 113 114 public static String getWeaponId(ShipAPI ship) { 115 if (isHighFrequency(ship)) return MOTELAUNCHER_HF; 116 return MOTELAUNCHER; 117 } 118 119 public static float getAntiFighterDamage(ShipAPI ship) { 120 return MOTE_DATA.get(getWeaponId(ship)).antiFighterDamage; 121 } 122 public static String getImpactSoundId(ShipAPI ship) { 123 return MOTE_DATA.get(getWeaponId(ship)).impactSound; 124 } 125 public static Color getJitterColor(ShipAPI ship) { 126 return MOTE_DATA.get(getWeaponId(ship)).jitterColor; 127 } 128 public static Color getEMPColor(ShipAPI ship) { 129 return MOTE_DATA.get(getWeaponId(ship)).empColor; 130 } 131 132 public static int getMaxMotes(ShipAPI ship) { 133 return MOTE_DATA.get(getWeaponId(ship)).maxMotes; 134 } 135 136 public static String getLoopSound(ShipAPI ship) { 137 return MOTE_DATA.get(getWeaponId(ship)).loopSound; 138 } 139 140 141 public static class SharedMoteAIData { 142 public float elapsed = 0f; 143 public List<MissileAPI> motes = new ArrayList<MissileAPI>(); 144 145 public float attractorRemaining = 0f; 146 public Vector2f attractorTarget = null; 147 public ShipAPI attractorLock = null; 148 } 149 150 public static SharedMoteAIData getSharedData(ShipAPI source) { 151 String key = source + "_mote_AI_shared"; 152 SharedMoteAIData data = (SharedMoteAIData)Global.getCombatEngine().getCustomData().get(key); 153 if (data == null) { 154 data = new SharedMoteAIData(); 155 Global.getCombatEngine().getCustomData().put(key, data); 156 } 157 return data; 158 } 159 160 161 162 protected IntervalUtil launchInterval = new IntervalUtil(0.75f, 1.25f); 163 protected IntervalUtil attractorParticleInterval = new IntervalUtil(0.05f, 0.1f); 164 protected WeightedRandomPicker<WeaponSlotAPI> launchSlots = new WeightedRandomPicker<WeaponSlotAPI>(); 165 protected WeaponSlotAPI attractor = null; 166 167 //protected int empCount = 0; 168 protected boolean findNewTargetOnUse = true; 169 170 protected void findSlots(ShipAPI ship) { 171 if (attractor != null) return; 172 for (WeaponSlotAPI slot : ship.getHullSpec().getAllWeaponSlotsCopy()) { 173 if (slot.isSystemSlot()) { 174 if (slot.getSlotSize() == WeaponSize.SMALL) { 175 launchSlots.add(slot); 176 } 177 if (slot.getSlotSize() == WeaponSize.MEDIUM) { 178 attractor = slot; 179 } 180 } 181 } 182 } 183 184 public void apply(MutableShipStatsAPI stats, String id, State state, float effectLevel) { 185 ShipAPI ship = null; 186 if (stats.getEntity() instanceof ShipAPI) { 187 ship = (ShipAPI) stats.getEntity(); 188 } else { 189 return; 190 } 191 192 float amount = Global.getCombatEngine().getElapsedInLastFrame(); 193 194 //Global.getCombatEngine().setPaused(true); 195 196 SharedMoteAIData data = getSharedData(ship); 197 data.elapsed += amount; 198 199 if (data.attractorRemaining > 0) { 200 data.attractorRemaining -= amount; 201 if (data.attractorRemaining <= 0 || 202 (data.attractorLock != null && !data.attractorLock.isAlive()) || 203 data.motes.isEmpty()) { 204 data.attractorTarget = null; 205 data.attractorLock = null; 206 data.attractorRemaining = 0; 207 } 208 } 209 if (effectLevel <= 0) { 210 findNewTargetOnUse = true; 211 } 212 213 CombatEngineAPI engine = Global.getCombatEngine(); 214 215 attractorParticleInterval.advance(amount); 216 if (attractorParticleInterval.intervalElapsed()) { 217 spawnAttractorParticles(ship); 218 } 219 220 launchInterval.advance(amount * 5f); 221 if (launchInterval.intervalElapsed()) { 222 Iterator<MissileAPI> iter = data.motes.iterator(); 223 while (iter.hasNext()) { 224 if (!engine.isMissileAlive(iter.next())) { 225 iter.remove(); 226 } 227 } 228 229 if (ship.isHulk()) { 230 for (MissileAPI mote : data.motes) { 231 mote.flameOut(); 232 } 233 data.motes.clear(); 234 return; 235 } 236 237 int maxMotes = getMaxMotes(ship); 238 if (data.motes.size() < maxMotes && data.attractorLock == null &&// false && 239 !ship.getFluxTracker().isOverloadedOrVenting()) { 240 findSlots(ship); 241 242 WeaponSlotAPI slot = launchSlots.pick(); 243 244 Vector2f loc = slot.computePosition(ship); 245 float dir = slot.computeMidArcAngle(ship); 246 float arc = slot.getArc(); 247 dir += arc * (float) Math.random() - arc /2f; 248 249 String weaponId = getWeaponId(ship); 250 MissileAPI mote = (MissileAPI) engine.spawnProjectile(ship, null, 251 weaponId, 252 loc, dir, null); 253 mote.setWeaponSpec(weaponId); 254 mote.setMissileAI(new MoteAIScript(mote)); 255 mote.getActiveLayers().remove(CombatEngineLayers.FF_INDICATORS_LAYER); 256 // if they could flame out/be affected by emp, that'd be bad since they don't expire for a 257 // very long time so they'd be stuck disabled permanently, for practical purposes 258 // thus: total emp resistance (which can't target them anyway, but if it did.) 259 mote.setEmpResistance(10000); 260 data.motes.add(mote); 261 262 engine.spawnMuzzleFlashOrSmoke(ship, slot, mote.getWeaponSpec(), 0, dir); 263 264 Global.getSoundPlayer().playSound("mote_attractor_launch_mote", 1f, 0.25f, loc, new Vector2f()); 265 } 266 } 267 268 float maxMotes = getMaxMotes(ship); 269 float fraction = data.motes.size() / (Math.max(1f, maxMotes)); 270 float volume = fraction * 3f; 271 if (volume > 1f) volume = 1f; 272 if (data.motes.size() > 3) { 273 Vector2f com = new Vector2f(); 274 for (MissileAPI mote : data.motes) { 275 Vector2f.add(com, mote.getLocation(), com); 276 } 277 com.scale(1f / data.motes.size()); 278 //Global.getSoundPlayer().playLoop("mote_attractor_loop", ship, 1f, volume, com, new Vector2f()); 279 Global.getSoundPlayer().playLoop(getLoopSound(ship), ship, 1f, volume, com, new Vector2f()); 280 } 281 282 283 if (effectLevel > 0 && findNewTargetOnUse) { 284 calculateTargetData(ship); 285 findNewTargetOnUse = false; 286 } 287 288 if (effectLevel == 1) { 289 // possible if system is reused immediately w/ no time to cool down, I think 290 if (data.attractorTarget == null) { 291 calculateTargetData(ship); 292 } 293 findSlots(ship); 294 295 Vector2f slotLoc = attractor.computePosition(ship); 296 297 CombatEntityAPI asteroid = engine.spawnAsteroid(0, data.attractorTarget.x, data.attractorTarget.y, 0, 0); 298 asteroid.setCollisionClass(CollisionClass.NONE); 299 CombatEntityAPI target = asteroid; 300 if (data.attractorLock != null) { 301 target = data.attractorLock; 302 } 303 304 EmpArcParams params = new EmpArcParams(); 305// params.segmentLengthMult = 4f; 306// params.zigZagReductionFactor = 0.25f; 307// params.fadeOutDist = 200f; 308// params.minFadeOutMult = 2f; 309 310 params.segmentLengthMult = 8f; 311 params.zigZagReductionFactor = 0.15f; 312 313 params.brightSpotFullFraction = 0.5f; 314 params.brightSpotFadeFraction = 0.5f; 315 //params.nonBrrightSpotMinBrightness = 0.25f; 316 317 float dist = Misc.getDistance(slotLoc, target.getLocation()); 318 params.flickerRateMult = 0.6f - dist / 3000f; 319 if (params.flickerRateMult < 0.3f) { 320 params.flickerRateMult = 0.3f; 321 } 322 323 324// params.fadeOutDist = 500f; 325// params.minFadeOutMult = 2f; 326// params.flickerRateMult = 0.7f; 327 //params.movementDurMax = 0.1f; 328// params.movementDurMin = 0.25f; 329// params.movementDurMax = 0.25f; 330 331 float emp = 0; 332 float dam = 0; 333 EmpArcEntityAPI arc = (EmpArcEntityAPI)engine.spawnEmpArc(ship, slotLoc, ship, target, 334 DamageType.ENERGY, 335 dam, 336 emp, // emp 337 100000f, // max range 338 "mote_attractor_targeted_ship", 339 40f, // thickness 340 //new Color(100,165,255,255), 341 MoteControlScript.getEMPColor(ship), 342 new Color(255,255,255,255), 343 params 344 ); 345 if (data.attractorLock != null) { 346 arc.setTargetToShipCenter(slotLoc, data.attractorLock); 347 } 348 arc.setCoreWidthOverride(30f); 349 350 //arc.setFadedOutAtStart(true); 351 //arc.setRenderGlowAtStart(false); 352 arc.setSingleFlickerMode(true); 353 354 if (data.attractorLock == null) { 355 Global.getSoundPlayer().playSound("mote_attractor_targeted_empty_space", 1f, 1f, data.attractorTarget, new Vector2f()); 356 } 357 358// Vector2f targetLoc = new Vector2f(target.getLocation()); 359// int glows = 100; 360// float maxRadius = 500f; 361// float minRadius = 300f; 362// 363// if (data.attractorLock != null) { 364// maxRadius += data.attractorLock.getCollisionRadius(); 365// minRadius += data.attractorLock.getCollisionRadius(); 366// } 367// 368// float minDur = 0.5f; 369// float maxDur = 0.75f; 370// float minSize = 10f; 371// float maxSize = 30f; 372// Color color = MoteControlScript.getEMPColor(ship); 373// for (int i = 0; i < glows; i++) { 374// float radius = minRadius + (float) Math.random() * (maxRadius - minRadius); 375// Vector2f loc = Misc.getPointAtRadius(targetLoc, radius); 376// Vector2f dir = Misc.getUnitVectorAtDegreeAngle(Misc.getAngleInDegrees(loc, targetLoc)); 377// float dist = Misc.getDistance(loc, targetLoc); 378// 379// float dur = minDur + (float) Math.random() * (maxDur - minDur); 380// float speed = dist / dur; 381// dir.scale(speed); 382// 383// float size = minSize + (float) Math.random() * (maxSize - minSize); 384// 385// engine.addHitParticle(loc, dir, size, 1f, 0.3f, dur, color); 386// engine.addHitParticle(loc, dir, size * 0.33f, 1f, 0.3f, dur, Color.white); 387// } 388 389 390 engine.removeEntity(asteroid); 391 } 392 } 393 394 protected void spawnAttractorParticles(ShipAPI ship) { 395 if (true) return; // just not liking this much 396 SharedMoteAIData data = getSharedData(ship); 397 398 if (data.attractorTarget == null) return; 399 400 CombatEngineAPI engine = Global.getCombatEngine(); 401 402 Vector2f targetLoc = data.attractorTarget; 403 404 int glows = 2; 405 float maxRadius = 300f; 406 float minRadius = 200f; 407 408 if (data.attractorLock != null) { 409 maxRadius += data.attractorLock.getCollisionRadius(); 410 minRadius += data.attractorLock.getCollisionRadius(); 411 targetLoc = data.attractorLock.getShieldCenterEvenIfNoShield(); 412 } 413 414 float minDur = 0.5f; 415 float maxDur = 0.75f; 416 float minSize = 15f; 417 float maxSize = 30f; 418 Color color = MoteControlScript.getEMPColor(ship); 419 for (int i = 0; i < glows; i++) { 420 float radius = minRadius + (float) Math.random() * (maxRadius - minRadius); 421 Vector2f loc = Misc.getPointAtRadius(targetLoc, radius); 422 Vector2f dir = Misc.getUnitVectorAtDegreeAngle(Misc.getAngleInDegrees(loc, targetLoc)); 423 float dist = Misc.getDistance(loc, targetLoc); 424 425 float dur = minDur + (float) Math.random() * (maxDur - minDur); 426 float speed = dist / dur; 427 dir.scale(speed); 428 429 float size = minSize + (float) Math.random() * (maxSize - minSize); 430 431 engine.addHitParticle(loc, dir, size, 1f, 0.3f, dur, color); 432 engine.addHitParticle(loc, dir, size * 0.5f, 1f, 0.3f, dur, Color.white); 433 } 434 } 435 436 437 public void unapply(MutableShipStatsAPI stats, String id) { 438 } 439 440 public StatusData getStatusData(int index, State state, float effectLevel) { 441 return null; 442 } 443 444 public void calculateTargetData(ShipAPI ship) { 445 SharedMoteAIData data = getSharedData(ship); 446 Vector2f targetLoc = getTargetLoc(ship); 447 //System.out.println(getTargetedLocation(ship)); 448 data.attractorLock = getLockTarget(ship, targetLoc); 449 450 data.attractorRemaining = ATTRACTOR_DURATION; 451 if (data.attractorLock != null) { 452 targetLoc = new Vector2f(data.attractorLock.getLocation()); 453 data.attractorRemaining = ATTRACTOR_DURATION_LOCK; 454 } 455 data.attractorTarget = targetLoc; 456 457 if (data.attractorLock != null) { 458 // need to do this in a script because when the ship is phased, the charge-in time of the system (0.1s) 459 // is not enough for the jitter to come to full effect (which requires 0.1s "normal" time) 460 Global.getCombatEngine().addPlugin(createTargetJitterPlugin(data.attractorLock, 461 ship.getSystem().getChargeUpDur(), ship.getSystem().getChargeDownDur(), 462 MoteControlScript.getJitterColor(ship))); 463 } 464 } 465 466 @Override 467 public String getInfoText(ShipSystemAPI system, ShipAPI ship) { 468 if (system.isOutOfAmmo()) return null; 469 if (system.getState() != SystemState.IDLE) return null; 470 471 boolean inRange = isMouseInRange(ship); 472 //Vector2f targetLoc = getTargetLoc(ship); 473 //ShipAPI target = getLockTarget(ship, targetLoc); 474 475 if (!inRange) { 476 return "OUT OF RANGE"; 477 } 478// if (target != null) { 479// return "ENEMY SHIP"; 480// } 481// return "AREA"; 482 return null; 483 } 484 485 486 @Override 487 public boolean isUsable(ShipSystemAPI system, ShipAPI ship) { 488 return true; 489 } 490 491 public Vector2f getTargetedLocation(ShipAPI from) { 492 Vector2f loc = from.getSystem().getTargetLoc(); 493 if (loc == null) { 494 loc = new Vector2f(from.getMouseTarget()); 495 } 496 return loc; 497 } 498 499 public Vector2f getTargetLoc(ShipAPI from) { 500 findSlots(from); 501 502 Vector2f slotLoc = attractor.computePosition(from); 503 Vector2f targetLoc = new Vector2f(getTargetedLocation(from)); 504 float dist = Misc.getDistance(slotLoc, targetLoc); 505 if (dist > getRange(from)) { 506 targetLoc = Misc.getUnitVectorAtDegreeAngle(Misc.getAngleInDegrees(slotLoc, targetLoc)); 507 targetLoc.scale(getRange(from)); 508 Vector2f.add(targetLoc, slotLoc, targetLoc); 509 } 510 return targetLoc; 511 } 512 513 public boolean isMouseInRange(ShipAPI from) { 514 Vector2f targetLoc = new Vector2f(from.getMouseTarget()); 515 return isLocationInRange(from, targetLoc); 516 } 517 518 public boolean isLocationInRange(ShipAPI from, Vector2f loc) { 519 findSlots(from); 520 521 Vector2f slotLoc = attractor.computePosition(from); 522 float dist = Misc.getDistance(slotLoc, loc); 523 if (dist > getRange(from)) { 524 return false; 525 } 526 return true; 527 } 528 529 530 public ShipAPI getLockTarget(ShipAPI from, Vector2f loc) { 531 Vector2f slotLoc = attractor.computePosition(from); 532 for (ShipAPI other : Global.getCombatEngine().getShips()) { 533 if (other.isFighter()) continue; 534 if (other.getOwner() == from.getOwner()) continue; 535 if (other.isHulk()) continue; 536 if (!other.isTargetable()) continue; 537 538 float dist = Misc.getDistance(slotLoc, other.getLocation()); 539 if (dist > getRange(from)) continue; 540 541 dist = Misc.getDistance(loc, other.getLocation()); 542 if (dist < other.getCollisionRadius() + 50f) { 543 return other; 544 } 545 } 546 return null; 547 } 548 549 public static float getRange(ShipAPI ship) { 550 if (ship == null) return MAX_ATTRACTOR_RANGE; 551 return ship.getMutableStats().getSystemRangeBonus().computeEffective(MAX_ATTRACTOR_RANGE); 552 } 553 554// public int getMaxMotes() { 555// return MAX_MOTES; 556// } 557 558 protected EveryFrameCombatPlugin createTargetJitterPlugin(final ShipAPI target, 559 final float in, 560 final float out, 561 final Color jitterColor) { 562 return new BaseEveryFrameCombatPlugin() { 563 float elapsed = 0f; 564 @Override 565 public void advance(float amount, List<InputEventAPI> events) { 566 if (Global.getCombatEngine().isPaused()) return; 567 568 elapsed += amount; 569 570 571 float level = 0f; 572 if (elapsed < in) { 573 level = elapsed / in; 574 } else if (elapsed < in + out) { 575 level = 1f - (elapsed - in) / out; 576 level *= level; 577 } else { 578 Global.getCombatEngine().removePlugin(this); 579 return; 580 } 581 582 583 if (level > 0) { 584 float jitterLevel = level; 585 float maxRangeBonus = 50f; 586 float jitterRangeBonus = jitterLevel * maxRangeBonus; 587 target.setJitterUnder(this, jitterColor, jitterLevel, 10, 0f, jitterRangeBonus); 588 target.setJitter(this, jitterColor, jitterLevel, 4, 0f, 0 + jitterRangeBonus); 589 } 590 } 591 }; 592 } 593} 594 595 596 597 598 599 600 601