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