001package com.fs.starfarer.api.impl.combat.threat; 002 003import java.util.ArrayList; 004import java.util.EnumSet; 005import java.util.LinkedHashMap; 006import java.util.LinkedHashSet; 007import java.util.List; 008import java.util.Set; 009 010import java.awt.Color; 011 012import org.lwjgl.util.vector.Vector2f; 013 014import com.fs.starfarer.api.Global; 015import com.fs.starfarer.api.combat.BaseCombatLayeredRenderingPlugin; 016import com.fs.starfarer.api.combat.CombatEngineLayers; 017import com.fs.starfarer.api.combat.CombatEntityAPI; 018import com.fs.starfarer.api.combat.MissileAPI; 019import com.fs.starfarer.api.combat.ShipAPI; 020import com.fs.starfarer.api.combat.ViewportAPI; 021import com.fs.starfarer.api.graphics.SpriteAPI; 022import com.fs.starfarer.api.util.FaderUtil; 023import com.fs.starfarer.api.util.IntervalUtil; 024import com.fs.starfarer.api.util.ListMap; 025import com.fs.starfarer.api.util.Misc; 026import com.fs.starfarer.api.util.WeightedRandomPicker; 027 028public class RoilingSwarmEffect extends BaseCombatLayeredRenderingPlugin { 029 030 public static interface SwarmMemberOffsetModifier { 031 void modifyOffset(SwarmMember p); 032 } 033 034 public static class RoilingSwarmParams { 035 public String spriteCat = "misc"; 036 public String spriteKey = "threat_swarm_pieces"; 037 public String despawnSound = "threat_swarm_destroyed"; 038 039 /** 040 * Set to non-null to exchange members with nearby swarms of the same class. 041 * Swarms should have the same sprite sheet, and the same glow color. 042 */ 043 public String memberExchangeClass = null; 044 public float memberExchangeRange = 500f; 045 public int minMembersToExchange = 1; 046 public int maxMembersToExchange = 3; 047 public float memberExchangeRate = 0.1f; 048 049 public String flockingClass = null; 050 051 public float baseSpriteSize = 20f; 052 053 public float baseDur = 100000f; 054 public float durRange = 0f; 055 public float despawnDist = 0f; 056 057 public float baseScale = 0.5f; 058 public float scaleRange = 0.5f; 059 public float baseFriction = 100f; 060 public float frictionRange = 500f; 061 public float baseSpringConstant = 50f; 062 public float springConstantNegativeRange = 20f; 063 public float baseSpringFreeLength = 20f; 064 public float springFreeLengthRange = 20f; 065 066 public float offsetRotationDegreesPerSecond = 0f; 067 068 public float lateralFrictionFactor = 20f; 069 public float lateralFrictionTurnRateFactor = 0; 070 //public float lateralFrictionFactor = 0.5f; 071 //public float lateralFrictionTurnRateFactor = 0.2f; 072 073 public float minSpeedForFriction = 25f; 074 075 public float flashRateMult = 1f; 076 public float flashRadius = 100f; 077 public float flashCoreRadiusMult = 1f; 078 public float flashFrequency = 1f; 079 public int numToFlash = 1; 080 public int numToRespawn = 1; 081 public float preFlashDelay = 0f; 082 public float flashProbability = 0f; 083 public boolean renderFlashOnSameLayer = false; 084 public Color flashFringeColor = new Color(255,0,0,255); 085 public Color flashCoreColor = Color.white; 086 087 public float alphaMult = 1f; 088 public float alphaMultBase = 1f; 089 public float alphaMultFlash = 1f; 090 public Color color = Color.white; 091 092 public float minFadeoutTime = 1f; 093 public float maxFadeoutTime = 1f; 094 public float minDespawnTime = 2f; 095 public float maxDespawnTime = 1f; 096 097 public boolean autoscale = false; 098 099 /** 100 * The amount of stretch is multiplied by this and then sqrt'ed. 101 */ 102 public float springStretchMult = 10f; 103 104 public float swarmLeadsByFractionOfVelocity = 0.03f; 105 106 public float outspeedAttachedEntityBy = 100f; 107 108 public float visibleRange = 500f; 109 public float maxTurnRate = 60f; 110 public float spawnOffsetMult = 0f; 111 public float spawnOffsetMultForInitialSpawn = -1f; 112 public float maxSpeed = 500f; 113 public float minOffset = 0f; 114 public float maxOffset = 20f; 115 public boolean generateOffsetAroundAttachedEntityOval = false; 116 117 public SwarmMemberOffsetModifier offsetModifier = null; 118 119 public boolean withInitialMembers = true; 120 public boolean withRespawn = true; 121 public int initialMembers = 50; 122 public int baseMembersToMaintain = 50; 123 public boolean removeMembersAboveMaintainLevel = true; 124 public int maxNumMembersToAlwaysRemoveAbove = -1; 125 public float memberRespawnRate = 1f; 126 public float offsetRerollFractionOnMemberRespawn = 0f; 127 128 public Set<String> tags = new LinkedHashSet<>(); 129 130 public boolean keepProxBasedScaleForAllMembers = false; 131 132 } 133 134 135 public static class SwarmMember { 136 public SpriteAPI sprite; 137 138 public Vector2f offset = new Vector2f(); 139 public Vector2f loc = new Vector2f(); 140 public Vector2f vel = new Vector2f(); 141 142 public float scale = 1f; 143 public float turnRate = 1f; 144 public float angle = 1f; 145 public float recentlyPicked = 0f; 146 147 public float dur; 148 public FaderUtil fader; 149 public FaderUtil flash; 150 public FaderUtil flashNext; 151 152 public FaderUtil scaler; 153 public float minScale; 154 public boolean keepScale = false; 155 156 public SwarmMember(Vector2f startingLoc, RoilingSwarmParams params, CombatEntityAPI attachedTo) { 157 //sprite = Global.getSettings().getSprite("misc", "nebula_particles"); 158 //fx_particles2 - swirly 159 // nebula_particles2 - smooth, but 2x2 160 // dust_particles - smooth 161 162 sprite = Global.getSettings().getSprite(params.spriteCat, params.spriteKey); 163 float i = Misc.random.nextInt(4); 164 float j = Misc.random.nextInt(4); 165 sprite.setTexWidth(0.25f); 166 sprite.setTexHeight(0.25f); 167 sprite.setTexX(i * 0.25f); 168 sprite.setTexY(j * 0.25f); 169 170 //sprite.setAdditiveBlend(); 171 sprite.setNormalBlend(); 172 173 angle = (float) Math.random() * 360f; 174 175 176 rollOffset(params, attachedTo); 177 178 Vector2f spawnOffset = new Vector2f(offset); 179 spawnOffset.scale(params.spawnOffsetMult); 180 if (params.spawnOffsetMult != 0) { 181 spawnOffset = Misc.rotateAroundOrigin(spawnOffset, attachedTo.getFacing()); 182 } 183 184 Vector2f.add(startingLoc, spawnOffset, loc); 185 186 vel = Misc.getPointWithinRadius(new Vector2f(), params.maxSpeed * 0.25f); 187 188// params.maxSpeed = 1200; 189// vel = new Vector2f(0, -1400f); 190// Vector2f.add(startingLoc, new Vector2f(0, 500f), loc); 191 192 dur = params.baseDur + (float) Math.random() * params.durRange; 193 scale = 1f; 194 195 turnRate = Math.signum((float) Math.random() - 0.5f) * params.maxTurnRate * (float) Math.random(); 196 197 fader = new FaderUtil(0f, 0.5f + (float) Math.random() * 0.5f, 198 params.minFadeoutTime + (params.maxFadeoutTime - params.minFadeoutTime) * (float) Math.random()); 199 fader.fadeIn(); 200 201 scaler = new FaderUtil(0f, 0.5f + (float) Math.random() * 0.5f, 0.5f + (float) Math.random() * 0.5f); 202 scaler.setBounce(true, true); 203 scaler.fadeIn(); 204 205 } 206 207 public void rollOffset(RoilingSwarmParams params, CombatEntityAPI attachedTo) { 208 if (params.generateOffsetAroundAttachedEntityOval && attachedTo instanceof ShipAPI) { 209 ShipAPI ship = (ShipAPI) attachedTo; 210 211 float angle = (float) Math.random() * 360f; 212 Vector2f dir = Misc.getUnitVectorAtDegreeAngle(angle); 213 Vector2f from = new Vector2f(dir); 214 from.scale(ship.getCollisionRadius() + 1000f); 215 Vector2f.add(from, ship.getLocation(), from); 216 217 float min = Misc.getTargetingRadius(from, ship, false); 218 float max = min + params.maxOffset; 219 min += params.minOffset; 220 221 float f = min/(Math.max(1f, max)); 222 f = Math.max(0.1f, f * 0.75f); 223 224 // there's definitely a smarter way than this to get a uniform distribution -am 225 float r = -1f; 226 for (int i = 0; i < 10; i++) { 227 float test = (float) Math.sqrt(Math.random()); 228 if (test >= f) { 229 r = test; 230 break; 231 } 232 } 233 if (r < 0f) { 234 r = f + (1f - f) * (float) Math.random(); 235 } 236 237 //dir.scale(min + (max - min) * r); 238 //r = f; 239 dir.scale(max * r); 240 offset = dir; 241 offset = Misc.rotateAroundOrigin(offset, -attachedTo.getFacing()); 242// if (params.spawnOffsetMultForInitialSpawn != params.spawnOffsetMult) { 243// offset = Misc.rotateAroundOrigin(offset, (float) Math.random() * 360f); 244// 245// } 246 //offset = new Vector2f(200f, 0f); 247 } else { 248 offset = Misc.getPointWithinRadiusUniform(new Vector2f(), params.minOffset, params.maxOffset, Misc.random); 249 } 250 251 if (params.offsetModifier != null) { 252 params.offsetModifier.modifyOffset(this); 253 } 254 } 255 256 public void advance(float amount, RoilingSwarmParams params) { 257 loc.x += vel.x * amount; 258 loc.y += vel.y * amount; 259 260 angle += turnRate * amount; 261 262 dur -= amount; 263 if (dur <= 0) fader.fadeOut(); 264 265 recentlyPicked -= amount; 266 if (recentlyPicked < 0) recentlyPicked = 0f; 267 268 fader.advance(amount); 269 if (flash != null) { 270 flash.advance(amount * params.flashRateMult); 271 if (flash.isFadedOut()) { 272 flash = null; 273 } 274 } 275 if (flash == null && flashNext != null) { 276 flash = flashNext; 277 flashNext = null; 278 } 279 280 if (params.autoscale && !keepScale) { 281 scaler.advance(amount * 0.5f); 282 scale = minScale + (1f - minScale) * scaler.getBrightness() * scaler.getBrightness(); 283 } 284 } 285 286 public void flash() { 287 if (flash == null) { 288 flash = new FaderUtil(0f, 0.25f, 1f); 289 flash.setBounceDown(true); 290 flash.fadeIn(); 291 } 292 } 293 294 public void flashNext() { 295 flashNext = new FaderUtil(0f, 0.25f, 1f); 296 flashNext.setBounceDown(true); 297 flashNext.fadeIn(); 298 } 299 300 public void setRecentlyPicked(float pickDuration) { 301 recentlyPicked = Math.max(recentlyPicked, pickDuration); 302 } 303 } 304 305 public static RoilingSwarmEffect getSwarmFor(CombatEntityAPI entity) { 306 if (entity == null) return null; 307 return getShipMap().get(entity); 308 } 309 310 public static String KEY_SHIP_MAP = "RoilingSwarmEffect_shipMap_key"; 311 public static String KEY_FLOCKING_MAP = "RoilingSwarmEffect_flockingMap_key"; 312 public static String KEY_EXCHANGE_MAP = "RoilingSwarmEffect_exchangeMap_key"; 313 314 @SuppressWarnings("unchecked") 315 public static LinkedHashMap<CombatEntityAPI, RoilingSwarmEffect> getShipMap() { 316 LinkedHashMap<CombatEntityAPI, RoilingSwarmEffect> map = 317 (LinkedHashMap<CombatEntityAPI, RoilingSwarmEffect>) Global.getCombatEngine().getCustomData().get(KEY_SHIP_MAP); 318 if (map == null) { 319 map = new LinkedHashMap<>(); 320 Global.getCombatEngine().getCustomData().put(KEY_SHIP_MAP, map); 321 } 322 return map; 323 } 324 public static ListMap<RoilingSwarmEffect> getFlockingMap() { 325 return getStringToSwarmMap(KEY_FLOCKING_MAP); 326 } 327 public static ListMap<RoilingSwarmEffect> getExchangeMap() { 328 return getStringToSwarmMap(KEY_EXCHANGE_MAP); 329 } 330 @SuppressWarnings("unchecked") 331 public static ListMap<RoilingSwarmEffect> getStringToSwarmMap(String key) { 332 ListMap<RoilingSwarmEffect> map = 333 (ListMap<RoilingSwarmEffect>) Global.getCombatEngine().getCustomData().get(key); 334 if (map == null) { 335 map = new ListMap<>(); 336 Global.getCombatEngine().getCustomData().put(key, map); 337 } 338 return map; 339 } 340 341 protected RoilingSwarmParams params; 342 protected List<SwarmMember> members = new ArrayList<SwarmMember>(); 343 344 protected CombatEntityAPI attachedTo; 345 protected float elapsed = 0f; 346 protected IntervalUtil flashChecker; 347 protected IntervalUtil respawnChecker; 348 protected IntervalUtil transferChecker; 349 protected boolean spawnedInitial = false; 350 protected boolean despawning = false; 351 protected boolean forceDespawn = false; 352 protected float sinceExchange = 0f; 353 protected float maxDistFromCenterToFragment = 0f; 354 355 public Object custom1; 356 public Object custom2; 357 public Object custom3; 358 359 360 public RoilingSwarmEffect(CombatEntityAPI attachedTo) { 361 this(attachedTo, new RoilingSwarmParams()); 362 } 363 364 public RoilingSwarmEffect(CombatEntityAPI attachedTo, RoilingSwarmParams params) { 365 //System.out.println("Creating swarm for " + attachedTo); 366 CombatEntityAPI e = Global.getCombatEngine().addLayeredRenderingPlugin(this); 367 e.getLocation().set(attachedTo.getLocation()); 368 369 this.attachedTo = attachedTo; 370 this.params = params; 371 372 // these values kinda work, too - a bit tigher 373 //params.maxOffset = 20; 374 //params.baseSpringFreeLength = 10; 375 376 //params.initialMembers = 1000; 377 //params.frictionRange = 0f; 378// params.lateralFrictionFactor = 1f; 379// params.baseSpringFreeLength = 0f; 380// params.springFreeLengthRange = 40f; 381 382// params.frictionRange = 100f; 383// params.baseFriction = 50f; 384// params.lateralFrictionFactor = 1f; 385 386// params.memberExchangeClass = "attack_swarm"; 387 388// params.baseDur = 5f; 389// params.durRange = 10f; 390// params.memberRespawnRate = 10f; 391 392 flashChecker = new IntervalUtil(0.5f, 1.5f); 393 respawnChecker = new IntervalUtil(0.5f, 1.5f); 394 transferChecker = new IntervalUtil(0.2f, 1.8f); 395 396 getShipMap().put(attachedTo, this); 397 if (params.flockingClass != null) { 398 getFlockingMap().add(params.flockingClass, this); 399 } 400 if (params.memberExchangeClass != null) { 401 getExchangeMap().add(params.memberExchangeClass, this); 402 } 403 } 404 405 public void init(CombatEntityAPI entity) { 406 super.init(entity); 407 } 408 409 public float getRenderRadius() { 410 float extra = 0f; 411 if (sinceExchange < 3f) { 412 extra = 500f - sinceExchange * 100f; 413 } 414 extra = Math.max(extra, maxDistFromCenterToFragment); 415 return params.visibleRange + extra; 416 } 417 418 419 protected EnumSet<CombatEngineLayers> layers = EnumSet.of(CombatEngineLayers.FIGHTERS_LAYER, 420 CombatEngineLayers.ABOVE_PARTICLES_LOWER); 421 @Override 422 public EnumSet<CombatEngineLayers> getActiveLayers() { 423 return layers; 424 } 425 426 public SwarmMember addMember() { 427 SwarmMember sm = new SwarmMember(attachedTo.getLocation(), params, attachedTo); 428 addMember(sm); 429 return sm; 430 } 431 public void addMember(SwarmMember sm) { 432 members.add(sm); 433 } 434 public void removeMember(SwarmMember sm) { 435 members.remove(sm); 436 } 437 public void addMembers(int num) { 438 for (int i = 0; i < num; i++) { 439 addMember(); 440 } 441 } 442 public void transferMembersTo(RoilingSwarmEffect other, float fraction) { 443 int num = (int) (members.size() * fraction); 444 transferMembersTo(other, num); 445 } 446 public void transferMembersTo(RoilingSwarmEffect other, int num) { 447 transferMembersTo(other, num, null, 0f); 448 } 449 public void transferMembersTo(RoilingSwarmEffect other, int num, Vector2f point, float maxRangeFromPoint) { 450 if (num <= 0) return; 451 WeightedRandomPicker<SwarmMember> picker = getPicker(true, true); 452 if (point != null) { 453 picker = getPicker(true, true, point, maxRangeFromPoint); 454 } 455 for (int i = 0; i < num; i++) { 456 SwarmMember pick = picker.pickAndRemove(); 457 if (pick == null) break; 458 459 removeMember(pick); 460 other.addMember(pick); 461 pick.rollOffset(other.params, other.attachedTo); 462 } 463 } 464 465 public void despawnMembers(int num) { 466 despawnMembers(num, true); 467 } 468 public void despawnMembers(int num, boolean allowFirst) { 469 WeightedRandomPicker<SwarmMember> picker = getPicker(false, false); 470 if (!allowFirst && !members.isEmpty()) { 471 picker.remove(members.get(0)); 472 } 473 for (int i = 0; i < num; i++) { 474 SwarmMember pick = picker.pickAndRemove(); 475 if (pick == null) break; 476 pick.fader.fadeOut(); 477 } 478 } 479 480 public SwarmMember pick(float pickDuration) { 481 SwarmMember pick = getPicker(true, true).pick(); 482 if (pick != null) { 483 pick.setRecentlyPicked(pickDuration); 484 } 485 return pick; 486 } 487 488 public WeightedRandomPicker<SwarmMember> getPicker(boolean preferNonFlashing, boolean preferNonPicked, 489 Vector2f towards) { 490 WeightedRandomPicker<SwarmMember> picker = new WeightedRandomPicker<>(); 491 float angle = Misc.getAngleInDegrees(attachedTo.getLocation(), towards); 492 for (SwarmMember p : members) { 493 if (p.fader.isFadingOut() || p.fader.isFadedOut()) continue; 494 float w = 1000f; 495 if (preferNonFlashing && p.flash != null) w *= 0.001f; 496 if (preferNonPicked && p.recentlyPicked > 0) w *= 0.001f; 497 498 float curr = Misc.getAngleInDegrees(attachedTo.getLocation(), p.loc); 499 float diff = Misc.getAngleDiff(angle, curr); 500 if (diff > 90f) { 501 float f = Misc.normalizeAngle(diff - 90f) / 90f; 502 if (f > 0.9999f) f = 0.9999f; 503 w *= 1f - f; 504 w *= 0.05f; 505 } 506 507 picker.add(p, w); 508 } 509 return picker; 510 } 511 public WeightedRandomPicker<SwarmMember> getPicker(boolean preferNonFlashing, boolean preferNonPicked, 512 Vector2f point, float preferMaxRangeFromPoint) { 513 WeightedRandomPicker<SwarmMember> picker = new WeightedRandomPicker<>(); 514 for (SwarmMember p : members) { 515 if (p.fader.isFadingOut() || p.fader.isFadedOut()) continue; 516 float w = 1000f; 517 if (preferNonFlashing && p.flash != null) w *= 0.001f; 518 if (preferNonPicked && p.recentlyPicked > 0) w *= 0.001f; 519 520 float dist = Misc.getDistance(point, p.loc); 521 if (dist > preferMaxRangeFromPoint) { 522 float f = (dist - preferMaxRangeFromPoint) / Math.max(1f, preferMaxRangeFromPoint); 523 if (f > 0.9999f) f = 0.9999f; 524 w *= 1f - f; 525 } else { 526 w *= 0.25f + 0.75f * (1f - dist / Math.max(1f, preferMaxRangeFromPoint)); 527 } 528 529 picker.add(p, w); 530 } 531 return picker; 532 } 533 public WeightedRandomPicker<SwarmMember> getPicker(boolean preferNonFlashing, boolean preferNonPicked) { 534 WeightedRandomPicker<SwarmMember> picker = new WeightedRandomPicker<>(); 535 for (SwarmMember p : members) { 536 if (p.fader.isFadingOut() || p.fader.isFadedOut()) continue; 537 float w = 1000f; 538 if (preferNonFlashing && p.flash != null) w *= 0.001f; 539 if (preferNonPicked && p.recentlyPicked > 0) w *= 0.001f; 540 picker.add(p, w); 541 } 542 return picker; 543 } 544 545 public int getNumActiveMembers() { 546 return getPicker(false, false).getItems().size(); 547 } 548 549 public float getGlowForMember(SwarmMember p) { 550 float glow = 0f; 551 if (p.flash != null) { 552 glow = p.flash.getBrightness(); 553 glow *= glow; 554 } 555 return glow; 556 } 557 558 public int getNumMembersToMaintain() { 559 return params.baseMembersToMaintain; 560 } 561 562 public void advance(float amount) { 563 //if (true) return; 564 565 if (Global.getCombatEngine().isPaused() || entity == null || isExpired()) return; 566 567 if (!spawnedInitial && params.withInitialMembers) { 568 float origSpawnOffsetMult = params.spawnOffsetMult; 569 if (params.spawnOffsetMultForInitialSpawn >= 0) { 570 params.spawnOffsetMult = params.spawnOffsetMultForInitialSpawn; 571 } 572 addMembers(params.initialMembers - getNumActiveMembers()); 573 params.spawnOffsetMult = origSpawnOffsetMult; 574 spawnedInitial = true; 575 } 576 577// attachedTo.setCollisionClass(CollisionClass.SHIP); 578// ((ShipAPI)attachedTo).getMutableStats().getHullDamageTakenMult().modifyMult("efwefwefwe", 0f); 579 580 //System.out.println("Swarm members: " + members.size()); 581 582 entity.getLocation().set(attachedTo.getLocation()); 583 584 elapsed += amount; 585 586 Vector2f aVel = attachedTo.getVelocity(); 587 float aSpeed = aVel.length(); 588 float leadAmount = aSpeed * params.swarmLeadsByFractionOfVelocity; 589 590 Vector2f facingDir = Misc.getUnitVectorAtDegreeAngle(attachedTo.getFacing()); 591 if (attachedTo.getVelocity().length() > 1f) { 592 facingDir = Misc.normalise(new Vector2f(attachedTo.getVelocity())); 593 } 594 595 Vector2f aLoc = new Vector2f(attachedTo.getLocation()); 596// if (params.generateOffsetAroundAttachedEntityOval && attachedTo instanceof ShipAPI) { 597// ShipAPI ship = (ShipAPI) attachedTo; 598// aLoc = new Vector2f(ship.getShieldCenterEvenIfNoShield()); 599// } 600 601 List<SwarmMember> remove = new ArrayList<>(); 602 603 float maxSpeed = params.maxSpeed; 604 if (params.outspeedAttachedEntityBy != 0) { 605 float minMaxSpeed = attachedTo.getVelocity().length() + params.outspeedAttachedEntityBy; 606 if (minMaxSpeed > maxSpeed) maxSpeed = minMaxSpeed; 607 } 608 609 // springs! (sort of, sqrt instead of linear) and friction 610 boolean despawnAll = shouldDespawnAll(); 611 612 float maxOffsetForProx = params.maxOffset; 613 if (params.generateOffsetAroundAttachedEntityOval) { 614 maxOffsetForProx += attachedTo.getCollisionRadius() * 0.75f; 615 } 616 617 618// int flashing = 0; 619// for (SwarmMember p : members) { 620// if (p.flash != null) flashing++; 621// } 622// System.out.println("Flashing: " + flashing + " / " + members.size()); 623 624 float maxDistSq = 0f; 625 maxDistFromCenterToFragment = 0f; 626 for (SwarmMember p : members) { 627 float distSq = (aLoc.x - p.loc.x) * (aLoc.x - p.loc.x) + (aLoc.y - p.loc.y) * (aLoc.y - p.loc.y); 628 maxDistSq = Math.max(maxDistSq, distSq); 629 if (params.despawnDist > 0 && params.despawnDist * params.despawnDist < distSq) { 630 p.fader.fadeOut(); 631 } 632 633 if (!despawnAll) { 634 Vector2f offset = new Vector2f(p.offset); 635 //offset.y *= p.offsetDrift; 636 //offset.y = p.offsetDrift * params.maxOffset; 637 //offset = Misc.rotateAroundOrigin(offset, attachedTo.getFacing() + elapsed * 5f); 638 639 float prox = offset.length() / maxOffsetForProx; 640 prox = 1f - prox; 641 642 643 offset = Misc.rotateAroundOrigin(offset, attachedTo.getFacing() + elapsed * params.offsetRotationDegreesPerSecond); 644 //offset = Misc.rotateAroundOrigin(offset, attachedTo.getFacing()); 645 offset.x += facingDir.x * leadAmount; 646 offset.y += facingDir.y * leadAmount; 647 648 if (!params.keepProxBasedScaleForAllMembers) { 649 p.scale = params.baseScale + (1f - prox) * params.scaleRange; 650 if (p.scale > 1f) p.scale = 1f; 651 } 652 653 Vector2f dest = new Vector2f(aLoc); 654 Vector2f.add(dest, offset, dest); 655 float dist = Misc.getDistance(p.loc, dest); 656 657 Vector2f dirToDest = Misc.getUnitVector(p.loc, dest); 658 Vector2f perp = new Vector2f(-dirToDest.y, dirToDest.x); 659 660 float friction = params.baseFriction + params.frictionRange * prox; 661 662 float k = params.baseSpringConstant - params.springConstantNegativeRange * prox; 663 float freeLength = params.baseSpringFreeLength + params.springFreeLengthRange * prox; 664 665 // if (proj == Global.getCombatEngine().getPlayerShip()) { 666 // System.out.println("32ferfwefw"); 667 // } 668 669 float stretch = dist - freeLength; 670 671 stretch = (float) (Math.sqrt(Math.abs(stretch * params.springStretchMult)) * Math.signum(stretch)); 672 673 float forceMag = k * Math.abs(stretch); 674 if (stretch < 0) forceMag = 0; // one-way spring, only pulls 675 676 float forceMagReduction = Math.min(Math.abs(forceMag), friction); 677 forceMag -= forceMagReduction; 678 friction -= forceMagReduction; 679 680 681 Vector2f force = new Vector2f(dirToDest); 682 force.scale(forceMag * Math.signum(stretch)); 683 684 Vector2f acc = new Vector2f(force); 685 acc.scale(amount); 686 Vector2f.add(p.vel, acc, p.vel); 687 688 // leftover friction - apply against current velocity 689 if (friction > 0) { 690 float relSpeed = Vector2f.sub(aVel, p.vel, new Vector2f()).length(); 691 if (relSpeed > params.minSpeedForFriction) { 692 Vector2f frictionDec = Misc.getUnitVectorAtDegreeAngle(Misc.getAngleInDegrees(p.vel)); 693 frictionDec.negate(); 694 frictionDec.scale(Math.min(friction, p.vel.length()) * amount); 695 Vector2f.add(p.vel, frictionDec, p.vel); 696 } 697 } 698 699 // lateral friction to damp out any orbiting behavior fast 700 float lateralSpeed = Math.abs(Vector2f.dot(p.vel, perp)); 701 if (lateralSpeed > 0) {// && lateralSpeed > params.minSpeedForFriction) { 702 Vector2f frictionDec = new Vector2f(perp); 703 if (Vector2f.dot(frictionDec, p.vel) > 0) { 704 frictionDec.negate(); 705 } 706 float lateralFactor = params.lateralFrictionFactor; 707 lateralFactor += Math.min(Math.abs(attachedTo.getAngularVelocity()), 100f) * params.lateralFrictionTurnRateFactor; 708 float lateralFriction = lateralSpeed * lateralFactor; 709 frictionDec.scale(Math.min(lateralFriction, p.vel.length()) * amount); 710 Vector2f.add(p.vel, frictionDec, p.vel); 711 } 712 713 714 715 float speed = p.vel.length(); 716 if (speed > maxSpeed) { 717 p.vel.scale(maxSpeed / speed); 718 } 719 720 } 721 722 p.advance(amount, params); 723 //p.loc.set(dest); 724 725 if (despawnAll) { 726 if (!p.fader.isFadingOut() && !p.fader.isFadedOut()) { 727 //p.fader.setDurationOut(2f + (float) Math.random() * 1f); 728 p.fader.setDurationOut(params.minDespawnTime + 729 (params.maxDespawnTime - params.minDespawnTime) * (float) Math.random()); 730 p.fader.fadeOut(); 731 } 732 } 733 734 735 if (p.fader.isFadedOut()) { 736 remove.add(p); 737 } 738 } 739 740 maxDistFromCenterToFragment = (float) Math.sqrt(maxDistSq); 741 742 members.removeAll(remove); 743 744 if (despawnAll) { 745 if (!despawning) { 746 if (params.despawnSound != null) { 747 Global.getSoundPlayer().playSound(params.despawnSound, 1f, 1f, entity.getLocation(), aVel); 748 despawning = true; 749 } 750 } 751 } 752 753 if (isExpired()) { 754// getShipMap().remove(attachedTo); 755// getFlockingMap().remove(params.flockingClass, this); 756// getExchangeMap().remove(params.memberExchangeClass, this); 757 } else if (!despawnAll && !despawning){ 758 exchangeWithNearbySwarms(amount); 759 } 760 761 762 if (!despawnAll) { 763 respawnChecker.advance(amount * params.memberRespawnRate); 764 if (respawnChecker.intervalElapsed() && params.withRespawn) { 765 int num = getNumMembersToMaintain(); 766 if (members.size() < num) { 767 int add = Math.min(params.numToRespawn, num - members.size()); 768 addMembers(add); 769 770 if (params.offsetRerollFractionOnMemberRespawn > 0f) { 771 int reroll = Math.round(params.offsetRerollFractionOnMemberRespawn * members.size()); 772 if (reroll < 1) reroll = 1; 773 WeightedRandomPicker<SwarmMember> picker = getPicker(true, false); 774 for (int i = 0; i < reroll; i++) { 775 SwarmMember pick = picker.pickAndRemove(); 776 if (pick == null) break; 777 pick.rollOffset(params, attachedTo); 778 } 779 } 780 781 } else if (members.size() > num && params.removeMembersAboveMaintainLevel) { 782 despawnMembers(1); 783 } else if (params.maxNumMembersToAlwaysRemoveAbove >= 0 && 784 members.size() > params.maxNumMembersToAlwaysRemoveAbove) { 785 int extra = members.size() - params.maxNumMembersToAlwaysRemoveAbove; 786 int numRemove = (int) Math.min(extra * 0.1f, 5f); 787 if (numRemove < 1) numRemove = 1; 788 despawnMembers(numRemove); 789 } 790 } 791 792 793 flashChecker.advance(amount * params.flashFrequency); 794 params.preFlashDelay -= amount; 795 if (params.preFlashDelay < 0) params.preFlashDelay = 0; 796 if (flashChecker.intervalElapsed() && params.preFlashDelay <= 0) { 797 if (params.flashProbability > 0) { 798 WeightedRandomPicker<SwarmMember> notFlashing = new WeightedRandomPicker<>(); 799 for (SwarmMember p : members) { 800 if (p.flash == null) { 801 notFlashing.add(p); 802 } 803 } 804 for (int i = 0; i < params.numToFlash; i++) { 805 if ((float) Math.random() < params.flashProbability) { 806 SwarmMember pick = notFlashing.pickAndRemove(); 807 if (pick != null) pick.flash(); 808 } 809 } 810 } 811 } 812 } 813 814 sinceExchange += amount; 815// if (proj.didDamage()) { 816// if (!resetTrailSpeed) { 817// for (ParticleData p : particles) { 818// Vector2f.add(p.vel, projVel, p.vel); 819// } 820// projVel.scale(0f); 821// resetTrailSpeed = true; 822// } 823// for (ParticleData p : particles) { 824// float dist = p.offset.length(); 825// p.vel.scale(Math.min(1f, dist / 100f)); 826// } 827// } 828 } 829 830 public void exchangeWithNearbySwarms(float amount) { 831 if (params.memberExchangeClass == null || params.memberExchangeRange <= 0) return; 832 833 transferChecker.advance(amount * params.memberExchangeRate); 834 if (!transferChecker.intervalElapsed()) return; 835 836 837 WeightedRandomPicker<RoilingSwarmEffect> swarmPicker = new WeightedRandomPicker<>(); 838 839 for (RoilingSwarmEffect other : getExchangeMap().getList(params.memberExchangeClass)) { 840 if (other == this || other.getEntity() == null || other.despawning) continue; 841 if (other.attachedTo == null || attachedTo == null) continue; 842 if (other.attachedTo.getOwner() != attachedTo.getOwner()) continue; 843 844 if (other.params.memberExchangeClass == null || 845 !other.params.memberExchangeClass.equals(params.memberExchangeClass)) { 846 continue; 847 } 848 float dist = Misc.getDistance(entity.getLocation(), other.getEntity().getLocation()); 849 if (dist > params.memberExchangeRange) continue; 850 851 swarmPicker.add(other); 852 } 853 854 RoilingSwarmEffect other = swarmPicker.pick(); 855 if (other == null) return; 856 857 int num = params.minMembersToExchange + 858 Misc.random.nextInt(params.maxMembersToExchange - params.minMembersToExchange + 1); 859 860 WeightedRandomPicker<SwarmMember> picker = getPicker(true, true); 861 WeightedRandomPicker<SwarmMember> pickerOther = other.getPicker(true, true); 862 863 num = Math.min(num, picker.getItems().size()); 864 num = Math.min(num, pickerOther.getItems().size()); 865 866 for (int i = 0; i < num; i++) { 867 SwarmMember pick = picker.pickAndRemove(); 868 SwarmMember otherPick = pickerOther.pickAndRemove(); 869 if (pick == null || otherPick == null) break; 870 871 removeMember(pick); 872 other.addMember(pick); 873 pick.rollOffset(other.params, other.attachedTo); 874 875 other.removeMember(otherPick); 876 addMember(otherPick); 877 otherPick.rollOffset(params, attachedTo); 878 879 sinceExchange = 0f; 880 } 881 882 } 883 884 885 public boolean shouldDespawnAll() { 886 if (forceDespawn) return true; 887 888// if ((float) Math.random() > 0.9995f && !params.generateOffsetAroundAttachedEntityOval) { 889// forceDespawn = true; 890// return true; 891// } 892 893 if (attachedTo instanceof ShipAPI) { 894 ShipAPI ship = (ShipAPI) attachedTo; 895 return !Global.getCombatEngine().isShipAlive(ship); 896 } 897 if (attachedTo instanceof MissileAPI) { 898 MissileAPI missile = (MissileAPI) attachedTo; 899 return !Global.getCombatEngine().isMissileAlive(missile); 900 } 901 902 return attachedTo.isExpired() || !Global.getCombatEngine().isEntityInPlay(attachedTo); 903 } 904 905 public boolean isExpired() { 906 boolean expired = shouldDespawnAll() && members.isEmpty(); 907 if (expired) { 908 //getFlockingMap().getList(FragmentSwarmHullmod.STANDARD_SWARM_FLOCKING_CLASS).get(0) 909 getShipMap().remove(attachedTo); 910 getFlockingMap().remove(params.flockingClass, this); 911 getExchangeMap().remove(params.memberExchangeClass, this); 912 } 913 return expired; 914 } 915 916 public void render(CombatEngineLayers layer, ViewportAPI viewport) { 917 //if (true) return; 918 919 //Color color = Color.white; 920 Color color = params.color; 921 float alphaMult = viewport.getAlphaMult(); 922 if (alphaMult <= 0f) return; 923 924 //alphaMult = 0.1f; 925 alphaMult *= params.alphaMult; 926 927 if (layer == CombatEngineLayers.FIGHTERS_LAYER) { 928// float zoom = viewport.getViewMult(); 929// //System.out.println("Zoom: " + zoom); 930// if (zoom >= 3f) { 931// GL11.glDisable(GL11.GL_TEXTURE_2D); 932// if (!members.isEmpty()) { 933// Color c = members.get(0).sprite.getAverageBrightColor(); 934// c = Misc.interpolateColor(c, members.get(0).sprite.getAverageColor(), 0.9f); 935// //Misc.setColor(c, alphaMult); 936// GL11.glEnable(GL11.GL_POINT_SMOOTH); 937// GL11.glPointSize(params.baseSpriteSize / zoom * 0.5f); 938// GL11.glBegin(GL11.GL_POINTS); 939// for (SwarmMember p : members) { 940//// float size = params.baseSpriteSize; 941//// size *= p.scale * p.fader.getBrightness(); 942// 943// float b = p.fader.getBrightness(); 944// Misc.setColor(c, alphaMult * b); 945// GL11.glVertex2f(p.loc.x, p.loc.y); 946// } 947// GL11.glEnd(); 948// } 949// } else { 950 if (!members.isEmpty()) { 951 members.get(0).sprite.bindTexture(); 952 } 953 for (SwarmMember p : members) { 954 float size = params.baseSpriteSize; 955 size *= p.scale * p.fader.getBrightness(); 956 957 float b = p.fader.getBrightness(); 958 //b *= 0.67f; 959 //b *= 0.5f; 960 //b *= 0.1f; 961 962 p.sprite.setAngle(p.angle); 963 p.sprite.setSize(size, size); 964 p.sprite.setAlphaMult(alphaMult * b * params.alphaMultBase); 965 p.sprite.setColor(color); 966 p.sprite.renderAtCenterNoBind(p.loc.x, p.loc.y); 967 968 float glow = getGlowForMember(p); 969 if (glow > 0 && params.flashCoreRadiusMult <= 0f) { 970 p.sprite.setAlphaMult(alphaMult * b * glow * params.alphaMultFlash); 971 p.sprite.setColor(params.flashCoreColor); 972 p.sprite.setAdditiveBlend(); 973 //p.sprite.setNormalBlend(); 974 p.sprite.renderAtCenter(p.loc.x, p.loc.y); 975 p.sprite.setNormalBlend(); 976 } 977 } 978// } 979 } 980 981 if ((layer == CombatEngineLayers.ABOVE_PARTICLES_LOWER && !params.renderFlashOnSameLayer) || 982 (layer == CombatEngineLayers.FIGHTERS_LAYER && params.renderFlashOnSameLayer)) { 983 SpriteAPI glowSprite = Global.getSettings().getSprite("misc", "threat_swarm_glow"); 984 glowSprite.setAdditiveBlend(); 985 for (SwarmMember p : members) { 986 float glow = getGlowForMember(p); 987 if (glow > 0f) { 988 float size = params.flashRadius * (0.5f + 0.5f * glow) * 2f; 989 size *= p.scale * p.fader.getBrightness(); 990 991// float f = p.offset.length() / 150f; 992// if (f > 1f) f = 1f; 993// //f = 1 - Math.min(f * 2f, 1f); 994// f = 1 - f * 0.5f; 995// //f = 0f; 996// Color color2 = Misc.interpolateColor(params.flashFringeColor, Misc.setBrightness( 997// new Color(7, 163, 169), 255), f); 998 999 float b = p.fader.getBrightness(); 1000 if (b > 0 && size > 0) { 1001 glowSprite.setSize(size, size); 1002 glowSprite.setAlphaMult(alphaMult * b * glow * 0.5f * params.alphaMultFlash); 1003 glowSprite.setColor(params.flashFringeColor); 1004 glowSprite.renderAtCenter(p.loc.x, p.loc.y); 1005 } 1006 1007 float memberSize = params.baseSpriteSize; 1008 memberSize *= p.scale; 1009 memberSize *= 2f; 1010 memberSize *= params.flashCoreRadiusMult; 1011 if (b > 0 && memberSize > 0) { 1012 glowSprite.setSize(memberSize, memberSize); 1013 glowSprite.setAlphaMult(alphaMult * b * p.fader.getBrightness() * glow * 0.5f * params.alphaMultFlash); 1014 glowSprite.setColor(params.flashCoreColor); 1015 glowSprite.renderAtCenter(p.loc.x, p.loc.y); 1016 } 1017 } 1018 } 1019 } 1020 } 1021 1022 public RoilingSwarmParams getParams() { 1023 return params; 1024 } 1025 public List<SwarmMember> getMembers() { 1026 return members; 1027 } 1028 public CombatEntityAPI getAttachedTo() { 1029 return attachedTo; 1030 } 1031 public boolean isDespawning() { 1032 return despawning; 1033 } 1034 public boolean isForceDespawn() { 1035 return forceDespawn; 1036 } 1037 public void setForceDespawn(boolean forceDespawn) { 1038 this.forceDespawn = forceDespawn; 1039 } 1040 1041 1042 1043} 1044 1045 1046 1047