001package com.fs.starfarer.api.impl.combat.threat;
002
003import java.util.ArrayList;
004import java.util.List;
005
006import java.awt.Color;
007
008import com.fs.starfarer.api.GameState;
009import com.fs.starfarer.api.Global;
010import com.fs.starfarer.api.campaign.CargoAPI.CargoItemType;
011import com.fs.starfarer.api.campaign.CargoStackAPI;
012import com.fs.starfarer.api.campaign.SpecialItemData;
013import com.fs.starfarer.api.combat.BaseHullMod;
014import com.fs.starfarer.api.combat.MutableShipStatsAPI;
015import com.fs.starfarer.api.combat.ShipAPI;
016import com.fs.starfarer.api.combat.ShipAPI.HullSize;
017import com.fs.starfarer.api.combat.WeaponAPI;
018import com.fs.starfarer.api.impl.campaign.ids.Items;
019import com.fs.starfarer.api.impl.campaign.ids.Stats;
020import com.fs.starfarer.api.impl.campaign.ids.Tags;
021import com.fs.starfarer.api.impl.combat.threat.RoilingSwarmEffect.RoilingSwarmParams;
022import com.fs.starfarer.api.loading.HullModSpecAPI;
023import com.fs.starfarer.api.util.ColorShifterUtil;
024import com.fs.starfarer.api.util.Misc;
025
026/**
027 * Hullmod that creates a fragment swarm around the ship. This swarm is required to power "fragment" weapons.
028 * 
029 * @author Alex
030 *
031 */
032public class FragmentSwarmHullmod extends BaseHullMod {
033        
034        public static String STANDARD_SWARM_EXCHANGE_CLASS = "standard_swarm_exchange_class";
035        public static String STANDARD_SWARM_FLOCKING_CLASS = "standard_swarm_flocking_class";
036        public static String CONSTRUCTION_SWARM_FLOCKING_CLASS = "construction_swarm_flocking_class";
037        public static String RECLAMATION_SWARM_FLOCKING_CLASS = "reclamation_swarm_flocking_class";
038        public static String RECLAMATION_SWARM_EXCHANGE_CLASS = "reclamation_swarm_exchange_class";
039        
040        public static boolean SHOW_OVERLAY_ON_THREAT_SHIPS = false;
041        
042        public static float SMOD_CR_PENALTY = 0.2f;
043        public static float SMOD_MAINTENANCE_PENALTY = 50f;
044        
045        public static Object STATUS_KEY1 = new Object();
046        
047        
048        @Override
049        public void applyEffectsBeforeShipCreation(HullSize hullSize, MutableShipStatsAPI stats, String id) {
050                boolean sMod = isSMod(stats);
051                if (sMod) {
052                        stats.getMaxCombatReadiness().modifyFlat(id, -Math.round(SMOD_CR_PENALTY * 100f) * 0.01f, "Fragment swarm");
053                        stats.getSuppliesPerMonth().modifyPercent(id, SMOD_MAINTENANCE_PENALTY);
054                }
055        }
056
057        @Override
058        public void applyEffectsAfterShipCreation(ShipAPI ship, String id) {
059                if (SHOW_OVERLAY_ON_THREAT_SHIPS || !ship.getHullSpec().hasTag(Tags.THREAT)) {
060                        ship.setExtraOverlay(Global.getSettings().getSpriteName("misc", "fragment_swarm"));
061                        ship.setExtraOverlayMatchHullColor(false);
062                        ship.setExtraOverlayShadowOpacity(1f);
063                }
064        }
065
066        @Override
067        public void advanceInCombat(ShipAPI ship, float amount) {
068                if (amount <= 0f || ship == null) return;
069                
070                RoilingSwarmEffect swarm = RoilingSwarmEffect.getSwarmFor(ship);
071                if (swarm == null) {
072                        swarm = createSwarmFor(ship);
073                }
074                
075                if (ship.isFighter()) return;
076                
077                boolean playerShip = Global.getCurrentState() == GameState.COMBAT &&
078                                Global.getCombatEngine() != null && Global.getCombatEngine().getPlayerShip() == ship;
079                
080                
081                RoilingSwarmParams params = swarm.params;
082                params.baseMembersToMaintain = (int) ship.getMutableStats().getDynamic().getValue(
083                                                                                Stats.FRAGMENT_SWARM_SIZE_MOD, getBaseSwarmSize(ship.getHullSize()));
084                params.memberRespawnRate = getBaseSwarmRespawnRateMult(ship.getHullSize()) * 
085                                                                ship.getMutableStats().getDynamic().getValue(Stats.FRAGMENT_SWARM_RESPAWN_RATE_MULT);
086                
087//              if (ship.getHullSpec().getHullId().equals(ThreatHullmod.HIVE_UNIT)) {
088//                      params.baseMembersToMaintain = SwarmLauncherEffect.FRAGMENT_NUM.get(SwarmLauncherEffect.ATTACK_SWARM_WING);
089//                      params.baseMembersToMaintain *= 8;
090//                      params.memberRespawnRate = 15 * ship.getMutableStats().getDynamic().getValue(Stats.FRAGMENT_SWARM_RESPAWN_RATE_MULT);
091//              }
092                
093                params.maxNumMembersToAlwaysRemoveAbove = (int) (params.baseMembersToMaintain * 1.5f);
094                params.initialMembers = params.baseMembersToMaintain;
095                
096                
097                if (playerShip) {
098                        int active = swarm.getNumActiveMembers();
099                        
100                        int maxRequired = 0;
101                        for (WeaponAPI w : ship.getAllWeapons()) {
102                                if (w.getEffectPlugin() instanceof FragmentWeapon) {
103                                        FragmentWeapon fw = (FragmentWeapon) w.getEffectPlugin();
104                                        maxRequired = Math.max(maxRequired, fw.getNumFragmentsToFire());
105                                }
106                        }
107                        
108                        boolean debuff = active < maxRequired;
109                        Global.getCombatEngine().maintainStatusForPlayerShip(STATUS_KEY1,
110                                        Global.getSettings().getSpriteName("ui", "icon_tactical_fragment_swarm"),
111                                        spec.getDisplayName(), 
112                                        "FRAGMENTS: " + active,
113                                        debuff);
114                }
115        }
116        
117        
118        public static RoilingSwarmEffect createSwarmFor(ShipAPI ship) {
119                RoilingSwarmEffect existing = RoilingSwarmEffect.getSwarmFor(ship);
120                if (existing != null) return existing;
121                
122//              if (true) {
123//                      return SwarmLauncherEffect.createTestDwellerSwarmFor(ship);
124//              }
125                
126                RoilingSwarmParams params = new RoilingSwarmParams();
127                if (ship.isFighter()) {
128                        float radius = 20f;
129                        int numMembers = 50;
130                        
131                        String wingId = ship.getWing() == null ? null : ship.getWing().getWingId();
132                        if (SwarmLauncherEffect.SWARM_RADIUS.containsKey(wingId)) {
133                                radius = SwarmLauncherEffect.SWARM_RADIUS.get(wingId); 
134                        }
135                        if (SwarmLauncherEffect.FRAGMENT_NUM.containsKey(wingId)) {
136                                numMembers = SwarmLauncherEffect.FRAGMENT_NUM.get(wingId); 
137                        }
138                        
139                        params.memberExchangeClass = STANDARD_SWARM_EXCHANGE_CLASS;
140                        params.flockingClass = FragmentSwarmHullmod.STANDARD_SWARM_FLOCKING_CLASS;
141                        params.maxSpeed = ship.getMaxSpeedWithoutBoost() + 
142                                                Math.max(ship.getMaxSpeedWithoutBoost() * 0.25f + 50f, 100f);
143                        
144                        params.flashRateMult = 0.25f;
145                        params.flashCoreRadiusMult = 0f;
146                        params.flashRadius = 120f;
147                        params.flashFringeColor = new Color(255,0,0,40);
148                        params.flashCoreColor = new Color(255,255,255,127);
149                        
150                        // if this is set to true and the swarm is glowing, missile-fragments pop over the glow and it looks bad
151                        //params.renderFlashOnSameLayer = true;
152                        
153                        params.maxOffset = radius;
154                        params.initialMembers = numMembers;
155                        params.baseMembersToMaintain = params.initialMembers;
156                } else {
157                        params.memberExchangeClass = STANDARD_SWARM_EXCHANGE_CLASS;
158                        params.maxSpeed = ship.getMaxSpeedWithoutBoost() + 
159                                                Math.max(ship.getMaxSpeedWithoutBoost() * 0.25f + 50f, 100f) +
160                                                ship.getMutableStats().getZeroFluxSpeedBoost().getModifiedValue();
161        
162                        params.flashRateMult = 0.25f;
163                        params.flashCoreRadiusMult = 0f;
164                        params.flashRadius = 120f;
165                        params.flashFringeColor = new Color(255,0,0,40);
166                        params.flashCoreColor = new Color(255,255,255,127);
167                        
168                        // if this is set to true and the swarm is glowing, missile-fragments pop over the glow and it looks bad
169                        //params.renderFlashOnSameLayer = true;
170                        
171                        params.minOffset = 0f;
172                        params.maxOffset = Math.min(100f, ship.getCollisionRadius() * 0.5f);
173                        params.generateOffsetAroundAttachedEntityOval = true;
174                        params.despawnSound = null; // ship explosion does the job instead
175                        params.spawnOffsetMult = 0.33f;
176                        params.spawnOffsetMultForInitialSpawn = 1f;
177                        
178                        params.baseMembersToMaintain = getBaseSwarmSize(ship.getHullSize());
179                        params.memberRespawnRate = getBaseSwarmRespawnRateMult(ship.getHullSize());
180                        params.maxNumMembersToAlwaysRemoveAbove = params.baseMembersToMaintain * 2;
181                        
182                        //params.offsetRerollFractionOnMemberRespawn = 0.05f;
183                        
184                        params.initialMembers = 0;
185                        params.initialMembers = params.baseMembersToMaintain;
186                        params.removeMembersAboveMaintainLevel = false;
187                }
188                
189                List<WeaponAPI> glowWeapons = new ArrayList<>();
190                for (WeaponAPI w : ship.getAllWeapons()) {
191                        if (w.usesAmmo() && w.getSpec().hasTag(Tags.FRAGMENT_GLOW)) {
192                                glowWeapons.add(w);
193                        }
194                        if (w.getSpec().hasTag(Tags.OVERSEER_CHARGE) || 
195                                        (ship.isFighter() && w.getSpec().hasTag(Tags.OVERSEER_CHARGE_FIGHTER))) {
196                                w.setAmmo(0);
197                        }
198                }
199                
200//              if (ship.hasTag(Tags.FRAGMENT_SWARM_START_WITH_ZERO_FRAGMENTS)) {
201//                      params.initialMembers = 0;
202//              }
203                
204                return new RoilingSwarmEffect(ship, params) {
205                        protected ColorShifterUtil glowColorShifter = new ColorShifterUtil(new Color(0, 0, 0, 0));
206                        protected boolean resetFlash = false;
207                        
208                        @Override
209                        public int getNumMembersToMaintain() {
210                                if (ship.isFighter()) {
211                                        return (int)Math.round(((0.2f + 0.8f * ship.getHullLevel()) * super.getNumMembersToMaintain()));
212                                }
213                                return super.getNumMembersToMaintain();
214                        }
215
216                        @Override
217                        public void advance(float amount) {
218                                super.advance(amount);
219                                
220                                glowColorShifter.advance(amount);
221                                
222                                // this is actually QUITE performance-intensive on the rendering, at least doubles the cost per swarm
223                                // (comment was from when flashFrequency was *10 with a shorter flashRateMult; *2 is pretty ok -am
224                                if (VoltaicDischargeOnFireEffect.isSwarmPhaseMode(ship)) {
225                                        params.flashFrequency = 4f;
226                                        params.flashProbability = 1f;
227                                        resetFlash = true;
228                                } else {
229                                        if (!glowWeapons.isEmpty()) {
230                                                float ammoFractionTotal = 0f;
231                                                float totalOP = 0f;
232                                                for (WeaponAPI w : glowWeapons) {
233                                                        float f = w.getAmmo() / Math.max(1f, w.getMaxAmmo());
234                                                        Color glowColor = w.getSpec().getGlowColor();
235        //                                              if (f > 0) {
236        //                                                      glowColorShifter.shift(w, glowColor, 0.5f, 0.5f, 1f);
237        //                                              }
238                                                        glowColorShifter.shift(w, glowColor, 0.5f, 0.5f, 1f);
239                                                        float weight = w.getSpec().getOrdnancePointCost(null);
240                                                        ammoFractionTotal += f * weight;
241                                                        totalOP += weight;
242                                                }
243                                                
244                                                float ammoFraction = ammoFractionTotal / Math.max(1f, totalOP);
245                                                params.flashFrequency = (1f + ammoFraction) * 2f;
246                                                params.flashFrequency *= Math.max(1f, Math.min(2f, params.baseMembersToMaintain / 50f));
247                                                params.flashProbability = 1f;
248                                                if (ammoFraction <= 0f) {
249                                                        params.flashProbability = 0f;
250                                                }
251                                                //params.flashFringeColor = new Color(255,0,0,(int)(30f + 30f * ammoFraction));
252                                                //float glowAlphaBase = 50f;
253                                                float glowAlphaBase = 30f;
254                                                if (ship.isFighter()) {
255                                                        glowAlphaBase = 18f;
256                                                }
257                                                
258                                                float extraGlow = (totalOP - 10f) / 90f;
259                                                if (extraGlow < 0) extraGlow = 0;
260                                                if (extraGlow > 1f) extraGlow = 1f;
261                                                
262                                                int glowAlpha = (int)(glowAlphaBase + glowAlphaBase * (ammoFraction + extraGlow * 0.5f));
263                                                if (glowAlpha > 255) glowAlpha = 255;
264                                                //params.flashFringeColor = Misc.setAlpha(glowColorShifter.getCurr(), glowAlpha);
265                                                params.flashFringeColor = Misc.setBrightness(glowColorShifter.getCurr(), 255);
266                                                params.flashFringeColor = Misc.setAlpha(params.flashFringeColor, glowAlpha);
267                                                
268                                                resetFlash = true;
269                                        } else {
270                                                //if (ThreatSwarmAI.isAttackSwarm(ship)) {
271                                                if (resetFlash) {
272                                                        params.flashProbability = 0f;
273                                                        resetFlash = false;
274                                                }
275                                        }
276                                }
277                                
278//                              int flashing = 0;
279//                              for (SwarmMember p : members) {
280//                                      if (p.flash != null) {
281//                                              flashing++;
282//                                      }
283//                              }
284//                              System.out.println("Flashing: " + flashing + ", total: " + members.size());
285                        }
286                        
287                };
288        }
289        
290        
291        
292        
293        public static int getBaseSwarmSize(HullSize size) {
294                switch (size) {
295                case CAPITAL_SHIP: return 100;
296                case CRUISER: return 60;
297                case DESTROYER: return 40;
298                case FRIGATE: return 20;
299                case FIGHTER: return 50; 
300                case DEFAULT: return 20;
301                default: return 20;
302                }
303        }
304        
305        public static float getBaseSwarmRespawnRateMult(HullSize size) {
306                switch (size) {
307                case CAPITAL_SHIP: return 5f;
308                case CRUISER: return 3f;
309                case DESTROYER: return 2f;
310                case FRIGATE: return 1f;
311                case FIGHTER: return 0f; 
312                case DEFAULT: return 0f;
313                default: return 0f;
314                }
315        }
316        
317        
318        @Override
319        public CargoStackAPI getRequiredItem() {
320                return Global.getSettings().createCargoStack(CargoItemType.SPECIAL, 
321                                                                new SpecialItemData(Items.FRAGMENT_FABRICATOR, null), null);
322        }
323        
324        public static boolean hasShroudedHullmods(ShipAPI ship) {
325                if (ship == null || ship.getVariant() == null) return false;
326                for (String id : ship.getVariant().getHullMods()) {
327                        HullModSpecAPI spec = Global.getSettings().getHullModSpec(id);
328                        if (spec != null && spec.hasTag(Tags.SHROUDED)) return true;
329                }
330                return false;
331        }
332        
333        public boolean isApplicableToShip(ShipAPI ship) {
334                if (ship != null && ship.getHullSpec().isPhase()) {
335                        return false;
336                }
337                if (hasShroudedHullmods(ship)) return false;
338                
339                return true;
340        }
341        
342        public String getUnapplicableReason(ShipAPI ship) {
343                if (ship != null && ship.getHullSpec().isPhase()) {
344                        return "Can not be installed on a phase ship";
345                }
346                return "Incompatible with Shrouded hullmods";
347        }
348
349        public String getDescriptionParam(int index, HullSize hullSize) {
350                if (index == 0) return "" + (int)getBaseSwarmSize(HullSize.FRIGATE);
351                if (index == 1) return "" + (int)getBaseSwarmSize(HullSize.DESTROYER);
352                if (index == 2) return "" + (int)getBaseSwarmSize(HullSize.CRUISER);
353                if (index == 3) return "" + (int)getBaseSwarmSize(HullSize.CAPITAL_SHIP);
354                
355                if (index == 4) return "" + (int)getBaseSwarmRespawnRateMult(HullSize.FRIGATE);
356                if (index == 5) return "" + (int)getBaseSwarmRespawnRateMult(HullSize.DESTROYER);
357                if (index == 6) return "" + (int)getBaseSwarmRespawnRateMult(HullSize.CRUISER);
358                if (index == 7) return "" + (int)getBaseSwarmRespawnRateMult(HullSize.CAPITAL_SHIP);
359
360                return null;
361        }
362        
363        @Override
364        public String getSModDescriptionParam(int index, HullSize hullSize, ShipAPI ship) {
365                if (index == 0) return "" + (int) Math.round(SMOD_CR_PENALTY * 100f) + "%";
366                if (index == 1) return "" + (int) Math.round(SMOD_MAINTENANCE_PENALTY) + "%";
367                return null;
368        }
369        
370        @Override
371        public boolean isSModEffectAPenalty() {
372                return true;
373        }
374}
375
376
377
378
379
380
381
382
383
384
385