001package com.fs.starfarer.api.impl.campaign.terrain;
002
003import java.util.ArrayList;
004import java.util.EnumSet;
005import java.util.List;
006
007import java.awt.Color;
008
009import org.lwjgl.util.vector.Vector2f;
010
011import com.fs.starfarer.api.Global;
012import com.fs.starfarer.api.campaign.CampaignEngineLayers;
013import com.fs.starfarer.api.campaign.CampaignFleetAPI;
014import com.fs.starfarer.api.campaign.PlanetAPI;
015import com.fs.starfarer.api.campaign.SectorEntityToken;
016import com.fs.starfarer.api.campaign.TerrainAIFlags;
017import com.fs.starfarer.api.combat.ViewportAPI;
018import com.fs.starfarer.api.fleet.FleetMemberAPI;
019import com.fs.starfarer.api.fleet.FleetMemberViewAPI;
020import com.fs.starfarer.api.graphics.SpriteAPI;
021import com.fs.starfarer.api.impl.campaign.ids.Stats;
022import com.fs.starfarer.api.impl.campaign.ids.Tags;
023import com.fs.starfarer.api.impl.campaign.terrain.AuroraRenderer.AuroraRendererDelegate;
024import com.fs.starfarer.api.impl.campaign.terrain.FlareManager.Flare;
025import com.fs.starfarer.api.impl.campaign.terrain.FlareManager.FlareManagerDelegate;
026import com.fs.starfarer.api.loading.Description.Type;
027import com.fs.starfarer.api.ui.Alignment;
028import com.fs.starfarer.api.ui.TooltipMakerAPI;
029import com.fs.starfarer.api.util.Misc;
030
031public class StarCoronaTerrainPlugin extends BaseRingTerrain implements AuroraRendererDelegate, FlareManagerDelegate {
032        
033        public static final float CR_LOSS_MULT_GLOBAL = 0.25f;
034        
035        public static class CoronaParams extends RingParams {
036                public float windBurnLevel;
037                public float flareProbability;
038                public float crLossMult;
039                
040                public CoronaParams(float bandWidthInEngine, float middleRadius,
041                                SectorEntityToken relatedEntity,
042                                float windBurnLevel, float flareProbability, float crLossMult) {
043                        super(bandWidthInEngine, middleRadius, relatedEntity);
044                        this.windBurnLevel = windBurnLevel;
045                        this.flareProbability = flareProbability;
046                        this.crLossMult = crLossMult;
047                }
048        }
049        
050        transient protected SpriteAPI texture = null;
051        transient protected Color color;
052        
053        protected AuroraRenderer renderer;
054        protected FlareManager flareManager;
055        protected CoronaParams params;
056        
057        protected transient RangeBlockerUtil blocker = null;
058        
059        public void init(String terrainId, SectorEntityToken entity, Object param) {
060                super.init(terrainId, entity, param);
061                params = (CoronaParams) param;
062                name = params.name;
063                if (name == null) {
064                        name = "Corona";
065                }
066        }
067        
068        public String getNameForTooltip() {
069                return "Corona";
070        }
071        
072        @Override
073        protected Object readResolve() {
074                super.readResolve();
075                texture = Global.getSettings().getSprite("terrain", "aurora");
076                layers = EnumSet.of(CampaignEngineLayers.TERRAIN_7);
077                if (renderer == null) {
078                        renderer = new AuroraRenderer(this);
079                }
080                if (flareManager == null) {
081                        flareManager = new FlareManager(this);
082                }
083                if (blocker == null) {
084                        blocker = new RangeBlockerUtil(360, super.params.bandWidthInEngine + 1000f);
085                }
086                return this;
087        }
088        
089        Object writeReplace() {
090                return this;
091        }
092        
093        @Override
094        protected boolean shouldPlayLoopOne() {
095                return super.shouldPlayLoopOne() && !flareManager.isInActiveFlareArc(Global.getSector().getPlayerFleet());
096        }
097
098        @Override
099        protected boolean shouldPlayLoopTwo() {
100                return super.shouldPlayLoopTwo() && flareManager.isInActiveFlareArc(Global.getSector().getPlayerFleet());
101        }
102
103
104
105        transient private EnumSet<CampaignEngineLayers> layers = EnumSet.of(CampaignEngineLayers.TERRAIN_7);
106        public EnumSet<CampaignEngineLayers> getActiveLayers() {
107                return layers;
108        }
109
110        public CoronaParams getParams() {
111                return params;
112        }
113
114        public void advance(float amount) {
115                super.advance(amount);
116                renderer.advance(amount);
117                flareManager.advance(amount);
118                
119                if (amount > 0 && blocker != null) {
120                        blocker.updateLimits(entity, params.relatedEntity, 0.5f);
121                        blocker.advance(amount, 100f, 0.5f);
122                }
123        }
124
125        public void render(CampaignEngineLayers layer, ViewportAPI viewport) {
126                if (blocker != null && !blocker.wasEverUpdated()) {
127                        blocker.updateAndSync(entity, params.relatedEntity, 0.5f);
128                }
129                renderer.render(viewport.getAlphaMult());
130        }
131        
132        @Override
133        public float getRenderRange() {
134                Flare curr = flareManager.getActiveFlare();
135                if (curr != null) {
136                        float outerRadiusWithFlare = computeRadiusWithFlare(flareManager.getActiveFlare());
137                        return outerRadiusWithFlare + 200f;
138                }
139                return super.getRenderRange();
140        }
141        
142        @Override
143        public boolean containsPoint(Vector2f point, float radius) {
144                if (blocker != null && blocker.isAnythingShortened()) {
145                        float angle = Misc.getAngleInDegrees(this.entity.getLocation(), point);
146                        float dist = Misc.getDistance(this.entity.getLocation(), point);
147                        float max = blocker.getCurrMaxAt(angle);
148                        if (dist > max) return false;
149                }
150                
151                if (flareManager.isInActiveFlareArc(point)) {
152                        float outerRadiusWithFlare = computeRadiusWithFlare(flareManager.getActiveFlare());
153                        float dist = Misc.getDistance(this.entity.getLocation(), point);
154                        if (dist > outerRadiusWithFlare + radius) return false;
155                        if (dist + radius < params.middleRadius - params.bandWidthInEngine / 2f) return false;
156                        return true;
157                }
158                return super.containsPoint(point, radius);
159        }
160        
161        protected float computeRadiusWithFlare(Flare flare) {
162                float inner = getAuroraInnerRadius();
163                float outer = params.middleRadius + params.bandWidthInEngine * 0.5f;
164                float thickness = outer - inner;
165                
166                thickness *= flare.extraLengthMult;
167                thickness += flare.extraLengthFlat;
168                
169                return inner + thickness;
170        }
171        
172        @Override
173        protected float getExtraSoundRadius() {
174                float base = super.getExtraSoundRadius();
175                
176                float angle = Misc.getAngleInDegrees(params.relatedEntity.getLocation(), Global.getSector().getPlayerFleet().getLocation());
177                float extra = 0f;
178                if (flareManager.isInActiveFlareArc(angle)) {
179                        extra = computeRadiusWithFlare(flareManager.getActiveFlare()) - params.bandWidthInEngine;
180                }
181                //System.out.println("Extra: " + extra);
182                return base + extra;
183        }
184        
185
186        @Override
187        public void applyEffect(SectorEntityToken entity, float days) {
188                if (entity instanceof CampaignFleetAPI) {
189                        
190                        // larger sim step when not current location means fleets tend to get trapped in black holes
191                        // so: just don't apply its effects
192                        if (!entity.isInCurrentLocation() && this instanceof EventHorizonPlugin) {
193                                return;
194                        }
195                        
196                        CampaignFleetAPI fleet = (CampaignFleetAPI) entity;
197                        
198                        boolean inFlare = false;
199                        if (flareManager.isInActiveFlareArc(fleet)) {
200                                inFlare = true;
201                        }
202                        
203                        float intensity = getIntensityAtPoint(fleet.getLocation());
204                        if (intensity <= 0) return;
205                        
206                        if (fleet.hasTag(Tags.FLEET_IGNORES_CORONA)) return;
207
208                        String buffId = getModId();
209                        float buffDur = 0.1f;
210
211                        boolean protectedFromCorona = false;
212                        if (fleet.isInCurrentLocation() && 
213                                        Misc.getDistance(fleet, Global.getSector().getPlayerFleet()) < 500) {
214                                for (SectorEntityToken curr : fleet.getContainingLocation().getCustomEntitiesWithTag(Tags.PROTECTS_FROM_CORONA_IN_BATTLE)) {
215                                        float dist = Misc.getDistance(curr, fleet);
216                                        if (dist < curr.getRadius() + fleet.getRadius() + 10f) {
217                                                protectedFromCorona = true;
218                                                break;
219                                        }
220                                }
221                        }
222                        
223                        // CR loss and peak time reduction
224                        for (FleetMemberAPI member : fleet.getFleetData().getMembersListCopy()) {
225                                float recoveryRate = member.getStats().getBaseCRRecoveryRatePercentPerDay().getModifiedValue();
226                                float lossRate = member.getStats().getBaseCRRecoveryRatePercentPerDay().getBaseValue();
227                                
228                                float resistance = member.getStats().getDynamic().getValue(Stats.CORONA_EFFECT_MULT);
229                                if (protectedFromCorona) resistance = 0f;
230                                //if (inFlare) loss *= 2f;
231                                float lossMult = 1f;
232                                if (inFlare) lossMult = 2f;
233                                float adjustedLossMult = (0f + params.crLossMult * intensity * resistance * lossMult * CR_LOSS_MULT_GLOBAL);
234                                
235                                float loss = (-1f * recoveryRate + -1f * lossRate * adjustedLossMult) * days * 0.01f;
236                                float curr = member.getRepairTracker().getBaseCR();
237                                if (loss > curr) loss = curr;
238                                if (resistance > 0) { // not actually resistance, the opposite
239                                        if (inFlare) {
240                                                member.getRepairTracker().applyCREvent(loss, "flare", "Solar flare effect");
241                                        } else {
242                                                member.getRepairTracker().applyCREvent(loss, "corona", "Star corona effect");
243                                        }
244                                }
245                                
246                                // needs to be applied when resistance is 0 to immediately cancel out the debuffs (by setting them to 0)
247                                float peakFraction = 1f / Math.max(1.3333f, 1f + params.crLossMult * intensity);
248                                float peakLost = 1f - peakFraction;
249                                peakLost *= resistance;
250                                float degradationMult = 1f + (params.crLossMult * intensity * resistance) / 2f;
251                                member.getBuffManager().addBuffOnlyUpdateStat(new PeakPerformanceBuff(buffId + "_1", 1f - peakLost, buffDur));
252                                member.getBuffManager().addBuffOnlyUpdateStat(new CRLossPerSecondBuff(buffId + "_2", degradationMult, buffDur));
253                        }
254                        
255                        // "wind" effect - adjust velocity
256                        float maxFleetBurn = fleet.getFleetData().getBurnLevel();
257                        float currFleetBurn = fleet.getCurrBurnLevel();
258                        
259                        float maxWindBurn = params.windBurnLevel;
260                        if (inFlare) {
261                                maxWindBurn *= 2f;
262                        }
263                        
264                        
265                        float currWindBurn = intensity * maxWindBurn;
266                        float maxFleetBurnIntoWind = maxFleetBurn - Math.abs(currWindBurn);
267                        
268                        float angle = Misc.getAngleInDegreesStrict(this.entity.getLocation(), fleet.getLocation());
269                        Vector2f windDir = Misc.getUnitVectorAtDegreeAngle(angle);
270                        if (currWindBurn < 0) {
271                                windDir.negate();
272                        }
273                        
274                        Vector2f velDir = Misc.normalise(new Vector2f(fleet.getVelocity()));
275                        velDir.scale(currFleetBurn);
276                        
277                        float fleetBurnAgainstWind = -1f * Vector2f.dot(windDir, velDir);
278                        
279                        float accelMult = 0.5f;
280                        if (fleetBurnAgainstWind > maxFleetBurnIntoWind) {
281                                accelMult += 0.75f + 0.25f * (fleetBurnAgainstWind - maxFleetBurnIntoWind);
282                        }
283                        float fleetAccelMult = fleet.getStats().getAccelerationMult().getModifiedValue();
284                        if (fleetAccelMult > 0) {// && fleetAccelMult < 1) {
285                                accelMult /= fleetAccelMult;
286                        }
287                        
288                        float seconds = days * Global.getSector().getClock().getSecondsPerDay();
289                        
290                        Vector2f vel = fleet.getVelocity();
291                        windDir.scale(seconds * fleet.getAcceleration() * accelMult);
292                        fleet.setVelocity(vel.x + windDir.x, vel.y + windDir.y);
293                        
294                        Color glowColor = getAuroraColorForAngle(angle);
295                        int alpha = glowColor.getAlpha();
296                        if (alpha < 75) {
297                                glowColor = Misc.setAlpha(glowColor, 75);
298                        }
299                        // visual effects - glow, tail
300                        
301                        
302                        float dist = Misc.getDistance(this.entity.getLocation(), fleet.getLocation());
303                        float check = 100f;
304                        if (params.relatedEntity != null) check = params.relatedEntity.getRadius() * 0.5f;
305                        if (dist > check) {
306                                float durIn = 1f;
307                                float durOut = 10f;
308                                Misc.normalise(windDir);
309                                float sizeNormal = 5f + 10f * intensity;
310                                float sizeFlare = 10f + 15f * intensity;
311                                for (FleetMemberViewAPI view : fleet.getViews()) {
312                                        if (inFlare) {
313                                                view.getWindEffectDirX().shift(getModId() + "_flare", windDir.x * sizeFlare, durIn, durOut, 1f);
314                                                view.getWindEffectDirY().shift(getModId() + "_flare", windDir.y * sizeFlare, durIn, durOut, 1f);
315                                                view.getWindEffectColor().shift(getModId() + "_flare", glowColor, durIn, durOut, intensity);
316                                        } else {
317                                                view.getWindEffectDirX().shift(getModId(), windDir.x * sizeNormal, durIn, durOut, 1f);
318                                                view.getWindEffectDirY().shift(getModId(), windDir.y * sizeNormal, durIn, durOut, 1f);
319                                                view.getWindEffectColor().shift(getModId(), glowColor, durIn, durOut, intensity);
320                                        }
321                                }
322                        }
323                }
324        }
325        
326        public float getIntensityAtPoint(Vector2f point) {
327                float angle = Misc.getAngleInDegrees(params.relatedEntity.getLocation(), point);
328                float maxDist = params.bandWidthInEngine;
329                if (flareManager.isInActiveFlareArc(angle)) {
330                        maxDist = computeRadiusWithFlare(flareManager.getActiveFlare());
331                }
332                float minDist = params.relatedEntity.getRadius();
333                float dist = Misc.getDistance(point, params.relatedEntity.getLocation());
334                
335                if (dist > maxDist) return 0f;
336                
337                float intensity = 1f;
338                if (minDist < maxDist) {
339                        intensity = 1f - (dist - minDist) / (maxDist - minDist);
340                        //intensity = 0.5f + intensity * 0.5f;
341                        if (intensity < 0) intensity = 0;
342                        if (intensity > 1) intensity = 1;
343                }
344                
345                return intensity;
346        }
347        
348        
349        
350        @Override
351        public Color getNameColor() {
352                Color bad = Misc.getNegativeHighlightColor();
353                Color base = super.getNameColor();
354                //bad = Color.red;
355                return Misc.interpolateColor(base, bad, Global.getSector().getCampaignUI().getSharedFader().getBrightness() * 1f);
356        }
357
358        public boolean hasTooltip() {
359                return true;
360        }
361        
362        public void createTooltip(TooltipMakerAPI tooltip, boolean expanded) {
363                float pad = 10f;
364                float small = 5f;
365                Color gray = Misc.getGrayColor();
366                Color highlight = Misc.getHighlightColor();
367                Color fuel = Global.getSettings().getColor("progressBarFuelColor");
368                Color bad = Misc.getNegativeHighlightColor();
369                
370                tooltip.addTitle(name);
371                tooltip.addPara(Global.getSettings().getDescription(getTerrainId(), Type.TERRAIN).getText1(), pad);
372                
373                float nextPad = pad;
374                if (expanded) {
375                        tooltip.addSectionHeading("Travel", Alignment.MID, pad);
376                        nextPad = small;
377                }
378                tooltip.addPara("Reduces the combat readiness of " +
379                                "all ships in the corona at a steady pace.", nextPad);
380                tooltip.addPara("The heavy solar wind also makes the star difficult to approach.", pad);
381                tooltip.addPara("Occasional solar flare activity takes these effects to even more dangerous levels.", pad);
382                
383                if (expanded) {
384                        tooltip.addSectionHeading("Combat", Alignment.MID, pad);
385                        tooltip.addPara("Reduces the peak performance time of ships and increases the rate of combat readiness degradation in protracted engagements.", small);
386                }
387                
388                //tooltip.addPara("Does not stack with other similar terrain effects.", pad);
389        }
390        
391        public boolean isTooltipExpandable() {
392                return true;
393        }
394        
395        public float getTooltipWidth() {
396                return 350f;
397        }
398        
399        public String getTerrainName() {
400                if (flareManager.isInActiveFlareArc(Global.getSector().getPlayerFleet())) {
401                        return "Solar Flare";
402                }
403                return super.getTerrainName();
404        }
405        
406        public String getEffectCategory() {
407                return null; // to ensure multiple coronas overlapping all take effect
408                //return "corona_" + (float) Math.random();
409        }
410
411        public float getAuroraAlphaMultForAngle(float angle) {
412                return 1f;
413        }
414
415        public float getAuroraBandWidthInTexture() {
416                return 256f;
417                //return 512f;
418        }
419        
420        public float getAuroraTexPerSegmentMult() {
421                return 1f;
422                //return 2f;
423        }
424
425        public Vector2f getAuroraCenterLoc() {
426                return params.relatedEntity.getLocation();
427        }
428
429        public Color getAuroraColorForAngle(float angle) {
430                if (color == null) {
431                        if (params.relatedEntity instanceof PlanetAPI) {
432                                color = ((PlanetAPI)params.relatedEntity).getSpec().getCoronaColor();
433                                //color = Misc.interpolateColor(color, Color.white, 0.5f);
434                        } else {
435                                color = Color.white;
436                        }
437                        color = Misc.setAlpha(color, 25);
438                }
439                if (flareManager.isInActiveFlareArc(angle)) {
440                        return flareManager.getColorForAngle(color, angle);
441                }
442                return color;
443        }
444
445        public float getAuroraInnerRadius() {
446                return params.relatedEntity.getRadius() + 50f;
447        }
448
449        public float getAuroraOuterRadius() {
450                return params.middleRadius + params.bandWidthInEngine * 0.5f;
451        }
452
453        public float getAuroraShortenMult(float angle) {
454                return 0.85f + flareManager.getShortenMod(angle);
455        }
456        
457        public float getAuroraInnerOffsetMult(float angle) {
458                return flareManager.getInnerOffsetMult(angle);
459        }
460
461        public SpriteAPI getAuroraTexture() {
462                return texture;
463        }
464        
465        public RangeBlockerUtil getAuroraBlocker() {
466                return blocker;
467        }
468
469        public float getAuroraThicknessFlat(float angle) {
470//              float shorten = blocker.getShortenAmountAt(angle);
471//              if (shorten > 0) return -shorten;
472//              if (true) return -4000f;
473                
474                if (flareManager.isInActiveFlareArc(angle)) {
475                        return flareManager.getExtraLengthFlat(angle);
476                }
477                return 0;
478        }
479
480        public float getAuroraThicknessMult(float angle) {
481                if (flareManager.isInActiveFlareArc(angle)) {
482                        return flareManager.getExtraLengthMult(angle);
483                }
484                return 1f;
485        }
486        
487        
488        
489        
490
491        public List<Color> getFlareColorRange() {
492                List<Color> result = new ArrayList<Color>();
493                
494                if (params.relatedEntity instanceof PlanetAPI) {
495                        Color color = ((PlanetAPI)params.relatedEntity).getSpec().getCoronaColor();
496                        result.add(Misc.setAlpha(color, 255));
497                } else {
498                        result.add(Color.white);
499                }
500                //result.add(Misc.setAlpha(getAuroraColorForAngle(0), 127));
501                return result;
502        }
503        
504        public float getFlareArcMax() {
505                return 60;
506        }
507        
508        public float getFlareArcMin() {
509                return 30;
510        }
511
512        public float getFlareExtraLengthFlatMax() {
513                return 500;
514        }
515
516        public float getFlareExtraLengthFlatMin() {
517                return 200;
518        }
519
520        public float getFlareExtraLengthMultMax() {
521                return 1.5f;
522        }
523
524        public float getFlareExtraLengthMultMin() {
525                return 1;
526        }
527
528        public float getFlareFadeInMax() {
529                return 10f;
530        }
531
532        public float getFlareFadeInMin() {
533                return 3f;
534        }
535
536        public float getFlareFadeOutMax() {
537                return 10f;
538        }
539
540        public float getFlareFadeOutMin() {
541                return 3f;
542        }
543
544        public float getFlareOccurrenceAngle() {
545                return 0;
546        }
547
548        public float getFlareOccurrenceArc() {
549                return 360f;
550        }
551
552        public float getFlareProbability() {
553                return params.flareProbability;
554        }
555
556        public float getFlareSmallArcMax() {
557                return 20;
558        }
559
560        public float getFlareSmallArcMin() {
561                return 10;
562        }
563
564        public float getFlareSmallExtraLengthFlatMax() {
565                return 100;
566        }
567
568        public float getFlareSmallExtraLengthFlatMin() {
569                return 50;
570        }
571
572        public float getFlareSmallExtraLengthMultMax() {
573                return 1.05f;
574        }
575
576        public float getFlareSmallExtraLengthMultMin() {
577                return 1;
578        }
579
580        public float getFlareSmallFadeInMax() {
581                return 2f;
582        }
583
584        public float getFlareSmallFadeInMin() {
585                return 1f;
586        }
587
588        public float getFlareSmallFadeOutMax() {
589                return 2f;
590        }
591
592        public float getFlareSmallFadeOutMin() {
593                return 1f;
594        }
595
596        public float getFlareShortenFlatModMax() {
597                return 0.05f;
598        }
599
600        public float getFlareShortenFlatModMin() {
601                return 0.05f;
602        }
603
604        public float getFlareSmallShortenFlatModMax() {
605                return 0.05f;
606        }
607
608        public float getFlareSmallShortenFlatModMin() {
609                return 0.05f;
610        }
611
612        public int getFlareMaxSmallCount() {
613                return 3;
614        }
615
616        public int getFlareMinSmallCount() {
617                return 5;
618        }
619
620        public float getFlareSkipLargeProbability() {
621                return 0f;
622        }
623
624        public SectorEntityToken getFlareCenterEntity() {
625                return this.entity;
626        }
627        
628        public boolean hasAIFlag(Object flag) {
629                return flag == TerrainAIFlags.CR_DRAIN ||
630                                flag == TerrainAIFlags.BREAK_OTHER_ORBITS ||
631                                flag == TerrainAIFlags.EFFECT_DIMINISHED_WITH_RANGE;
632        }
633        
634        public float getMaxEffectRadius(Vector2f locFrom) {
635                float angle = Misc.getAngleInDegrees(params.relatedEntity.getLocation(), locFrom);
636                float maxDist = params.bandWidthInEngine;
637                if (flareManager.isInActiveFlareArc(angle)) {
638                        maxDist = computeRadiusWithFlare(flareManager.getActiveFlare());
639                }
640                return maxDist;
641        }
642        public float getMinEffectRadius(Vector2f locFrom) {
643                return 0f;
644        }
645        
646        public float getOptimalEffectRadius(Vector2f locFrom) {
647                return params.relatedEntity.getRadius();
648        }
649        
650        public boolean canPlayerHoldStationIn() {
651                return false;
652        }
653
654        public FlareManager getFlareManager() {
655                return flareManager;
656        }
657        
658}
659
660
661
662
663