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