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