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