001package com.fs.starfarer.api.impl.combat.dweller; 002 003import java.awt.Color; 004 005import org.lwjgl.util.vector.Vector2f; 006 007import com.fs.starfarer.api.Global; 008import com.fs.starfarer.api.campaign.CargoAPI.CargoItemType; 009import com.fs.starfarer.api.campaign.CargoStackAPI; 010import com.fs.starfarer.api.campaign.SpecialItemData; 011import com.fs.starfarer.api.combat.CollisionClass; 012import com.fs.starfarer.api.combat.CombatEngineAPI; 013import com.fs.starfarer.api.combat.CombatEntityAPI; 014import com.fs.starfarer.api.combat.DamageAPI; 015import com.fs.starfarer.api.combat.DamageType; 016import com.fs.starfarer.api.combat.DamagingProjectileAPI; 017import com.fs.starfarer.api.combat.EmpArcEntityAPI; 018import com.fs.starfarer.api.combat.EmpArcEntityAPI.EmpArcParams; 019import com.fs.starfarer.api.combat.ShipAPI; 020import com.fs.starfarer.api.combat.ShipAPI.HullSize; 021import com.fs.starfarer.api.combat.listeners.DamageDealtModifier; 022import com.fs.starfarer.api.impl.campaign.ids.Items; 023import com.fs.starfarer.api.impl.combat.NegativeExplosionVisual.NEParams; 024import com.fs.starfarer.api.impl.combat.RiftCascadeMineExplosion; 025import com.fs.starfarer.api.impl.combat.dweller.DwellerShroud.DwellerShroudParams; 026import com.fs.starfarer.api.impl.combat.threat.EnergyLashSystemScript.DelayedCombatActionPlugin; 027import com.fs.starfarer.api.loading.DamagingExplosionSpec; 028import com.fs.starfarer.api.ui.Alignment; 029import com.fs.starfarer.api.ui.TooltipMakerAPI; 030import com.fs.starfarer.api.util.Misc; 031import com.fs.starfarer.api.util.TimeoutTracker; 032import com.fs.starfarer.api.util.WeightedRandomPicker; 033 034public class ShroudedThunderheadHullmod extends HumanShipShroudedHullmod { 035 036 public static float MAX_RANGE = 3000f; 037 038 public static float RECENT_HIT_DUR = 5f; 039 public static float MAX_TIME_SINCE_RECENT_HIT = 0.1f; 040 public static float WEIGHT_PER_RECENT_HIT = 1f; 041 public static float MISFIRE_WEIGHT = 10f; 042 043 public static float MIN_REFIRE_DELAY = 0.22f; 044 public static float MAX_REFIRE_DELAY = 0.44f; 045 public static float REFIRE_RATE_MULT = 1f; 046 047 public static float FLUX_PER_DAMAGE = 1f; 048 public static float MIN_DAMAGE = 200f; 049 public static float MAX_DAMAGE = 500f; 050 public static float EMP_MULT = 2f; 051 052 public static class ShroudedThunderheadDamageDealtMod implements DamageDealtModifier { 053 public ShipAPI ship; 054 public ShroudedThunderheadDamageDealtMod(ShipAPI ship) { 055 this.ship = ship; 056 } 057 public String modifyDamageDealt(Object param, 058 CombatEntityAPI target, DamageAPI damage, 059 Vector2f point, boolean shieldHit) { 060 if (param instanceof DamagingProjectileAPI) { 061 DamagingProjectileAPI proj = (DamagingProjectileAPI) param; 062 DamagingExplosionSpec spec = proj.getExplosionSpecIfExplosion(); 063 if (spec != null && spec.getCollisionClassIfByFighter() == CollisionClass.GAS_CLOUD) { 064 return null; 065 } 066 } else if ((damage.isDps() && !damage.isForceHardFlux()) || damage.getDamage() <= 0f) { 067 return null; 068 } 069 070 if (target != null) { 071 ShroudedThunderheadHullmodData data = getData(ship); 072 RecentHitData hit = new RecentHitData(); 073 hit.param = param; 074 hit.target = target; 075 hit.point = new Vector2f(point); 076 hit.damage = damage; 077 hit.shieldHit = shieldHit; 078 //data.recentHits.add(hit, 1.5f); 079 data.recentHits.add(hit, RECENT_HIT_DUR); 080 } 081 return null; 082 } 083 } 084 085 @Override 086 public void applyEffectsAfterShipCreation(ShipAPI ship, String id) { 087 super.applyEffectsAfterShipCreation(ship, id); 088 ship.addListener(new ShroudedThunderheadDamageDealtMod(ship)); 089 } 090 091 public String getDescriptionParam(int index, HullSize hullSize) { 092 return null; 093 } 094 095 @Override 096 public CargoStackAPI getRequiredItem() { 097 return Global.getSettings().createCargoStack(CargoItemType.SPECIAL, 098 new SpecialItemData(Items.SHROUDED_THUNDERHEAD, null), null); 099 } 100 101 public static String DATA_KEY = "core_ShroudedThunderheadHullmod_data_key"; 102 public static class RecentHitData { 103 Object param; 104 CombatEntityAPI target; 105 Vector2f point; 106 DamageAPI damage; 107 boolean shieldHit; 108 } 109 public static class ShroudedThunderheadHullmodData { 110 float untilArc = 0f; 111 TimeoutTracker<RecentHitData> recentHits = new TimeoutTracker<RecentHitData>(); 112 113 boolean hasRecentEnoughHits() { 114 for (RecentHitData curr : recentHits.getItems()) { 115 float remaining = recentHits.getRemaining(curr); 116 if (remaining >= RECENT_HIT_DUR - MAX_TIME_SINCE_RECENT_HIT) return true; 117 } 118 return false; 119 } 120 121 float getHitProbability() { 122 float recent = recentHits.getItems().size() * WEIGHT_PER_RECENT_HIT; 123 return recent / (recent + MISFIRE_WEIGHT); 124 } 125 126 RecentHitData pickRecentHit() { 127 if (!hasRecentEnoughHits()) return null; 128 WeightedRandomPicker<RecentHitData> picker = new WeightedRandomPicker<>(); 129 for (RecentHitData curr : recentHits.getItems()) { 130 float remaining = recentHits.getRemaining(curr); 131 if (remaining < RECENT_HIT_DUR - MAX_TIME_SINCE_RECENT_HIT * 2f) continue; 132 picker.add(curr, WEIGHT_PER_RECENT_HIT); 133 } 134 RecentHitData misfire = new RecentHitData(); 135 picker.add(misfire, MISFIRE_WEIGHT); 136 return picker.pick(); 137 } 138 } 139 140 public static ShroudedThunderheadHullmodData getData(ShipAPI ship) { 141 CombatEngineAPI engine = Global.getCombatEngine(); 142 String key = DATA_KEY + "_" + ship.getId(); 143 ShroudedThunderheadHullmodData data = (ShroudedThunderheadHullmodData) engine.getCustomData().get(key); 144 if (data == null) { 145 data = new ShroudedThunderheadHullmodData(); 146 engine.getCustomData().put(key, data); 147 } 148 return data; 149 } 150 151 @Override 152 public void advanceInCombat(ShipAPI ship, float amount) { 153 super.advanceInCombat(ship, amount); 154 155 if (!ship.isAlive()) return; 156 if (amount <= 0f) return; 157 158 ShroudedThunderheadHullmodData data = getData(ship); 159 160 161 float prob = data.getHitProbability(); 162 DwellerShroud shroud = DwellerShroud.getShroudFor(ship); 163 shroud.getParams().flashProbability = Math.min(0.1f + prob * 1.4f, 1f); 164 //shroud.getParams().alphaMult = 0.5f; 165 166 data.untilArc -= amount * REFIRE_RATE_MULT; 167 if (data.untilArc <= 0f) { 168 boolean hasRecentHits = data.hasRecentEnoughHits(); 169 if (hasRecentHits) { 170 RecentHitData hit = data.pickRecentHit(); 171 if (hit != null && hit.target != null) { 172 data.recentHits.remove(hit); 173 spawnLightning(ship, hit); 174 } 175 data.untilArc = MIN_REFIRE_DELAY + (float) Math.random() * (MAX_REFIRE_DELAY - MIN_REFIRE_DELAY); 176 } 177 } 178 179 data.recentHits.advance(amount); 180 } 181 182 public static float getPowerMult(HullSize size) { 183 switch (size) { 184 case CAPITAL_SHIP: return 1f; 185 case CRUISER: return 0.6666666667f; 186 case DESTROYER: return 0.3333333333f; 187 case FIGHTER: 188 case FRIGATE: 189 return 0f; 190 } 191 return 1f; 192 } 193 194 public static float getDamage(HullSize size) { 195 float mult = getPowerMult(size); 196 return MIN_DAMAGE + (MAX_DAMAGE - MIN_DAMAGE) * mult; 197 } 198 public static float getEMPDamage(HullSize size) { 199 return getDamage(size) * EMP_MULT; 200 } 201 public static float getFluxCost(HullSize size) { 202 return getDamage(size) * FLUX_PER_DAMAGE; 203 } 204 205 public void spawnLightning(ShipAPI ship, RecentHitData hit) { 206 CombatEngineAPI engine = Global.getCombatEngine(); 207 208 Vector2f from = ship.getLocation(); 209 Vector2f point = hit.point; 210 211 float dist = Misc.getDistance(from, point); 212 213 if (dist > MAX_RANGE) return; 214 215 float mult = getPowerMult(ship.getHullSize()); 216 float damage = getDamage(ship.getHullSize()); 217 float emp = getEMPDamage(ship.getHullSize()); 218 219 if (FLUX_PER_DAMAGE > 0f) { 220 float fluxCost = getFluxCost(ship.getHullSize()); 221 if (!deductFlux(ship, fluxCost)) { 222 return; 223 } 224 } 225 226 DwellerShroud shroud = DwellerShroud.getShroudFor(ship); 227 if (shroud != null) { 228 float angle = Misc.getAngleInDegrees(ship.getLocation(), point); 229 from = Misc.getUnitVectorAtDegreeAngle(angle + 90f - 180f * (float) Math.random()); 230 from.scale((0.5f + (float) Math.random() * 0.25f) * shroud.getShroudParams().maxOffset * shroud.getShroudParams().overloadArcOffsetMult); 231 Vector2f.add(ship.getLocation(), from, from); 232 } 233 234 235 float arcSpeed = RiftLightningEffect.RIFT_LIGHTNING_SPEED; 236 237 EmpArcParams params = new EmpArcParams(); 238 params.segmentLengthMult = 8f; 239 params.zigZagReductionFactor = 0.15f; 240 //params.fadeOutDist = ship.getCollisionRadius() * 0.5f; 241 params.fadeOutDist = 50f; 242 params.minFadeOutMult = 10f; 243// params.flickerRateMult = 0.7f; 244 params.flickerRateMult = 0.3f; 245// params.flickerRateMult = 0.05f; 246// params.glowSizeMult = 3f; 247// params.brightSpotFullFraction = 0.5f; 248 249 params.movementDurOverride = Math.max(0.05f, dist / arcSpeed); 250 251 float arcWidth = 40f + mult * 40f; 252 float explosionRadius = 40f + mult * 40f; 253 254 //Color color = weapon.getSpec().getGlowColor(); 255 Color color = RiftLightningEffect.RIFT_LIGHTNING_COLOR; 256 EmpArcEntityAPI arc = (EmpArcEntityAPI)engine.spawnEmpArcVisual(from, ship, point, null, 257 arcWidth, // thickness 258 color, 259 new Color(255,255,255,255), 260 params 261 ); 262 arc.setCoreWidthOverride(arcWidth / 2f); 263 264 arc.setRenderGlowAtStart(false); 265 arc.setFadedOutAtStart(true); 266 arc.setSingleFlickerMode(true); 267 268 float volume = 0.75f + 0.25f * mult; 269 float pitch = 1f + 0.25f * (1f - mult); 270 Global.getSoundPlayer().playSound("rift_lightning_fire", pitch, volume, from, ship.getVelocity()); 271 272 if (shroud != null) { 273 DwellerShroudParams shroudParams = shroud.getShroudParams(); 274 params = new EmpArcParams(); 275 params.segmentLengthMult = 4f; 276 params.glowSizeMult = 4f; 277 params.flickerRateMult = 0.5f + (float) Math.random() * 0.5f; 278 params.flickerRateMult *= 1.5f; 279 280 //Color fringe = shroudParams.overloadArcFringeColor; 281 Color fringe = color; 282 Color core = Color.white; 283 284 float thickness = shroudParams.overloadArcThickness; 285 286 //Vector2f to = Misc.getPointAtRadius(from, 1f); 287 288 float angle = Misc.getAngleInDegrees(from, ship.getLocation()); 289 angle = angle + 90f * ((float) Math.random() - 0.5f); 290 Vector2f dir = Misc.getUnitVectorAtDegreeAngle(angle); 291 dist = shroudParams.maxOffset * shroud.getShroudParams().overloadArcOffsetMult; 292 dist = dist * 0.5f + dist * 0.5f * (float) Math.random(); 293 //dist *= 1.5f; 294 dist *= 0.5f; 295 dir.scale(dist); 296 Vector2f to = Vector2f.add(from, dir, new Vector2f()); 297 298 arc = (EmpArcEntityAPI)engine.spawnEmpArcVisual( 299 from, ship, to, ship, thickness, fringe, core, params); 300 301 arc.setCoreWidthOverride(shroudParams.overloadArcCoreThickness); 302 arc.setSingleFlickerMode(false); 303 //arc.setRenderGlowAtStart(false); 304 } 305 306 307 308 float explosionDelay = params.movementDurOverride * 0.8f; 309 Global.getCombatEngine().addPlugin(new DelayedCombatActionPlugin(explosionDelay, new Runnable() { 310 @Override 311 public void run() { 312 DamagingExplosionSpec spec = new DamagingExplosionSpec( 313 0.1f, // duration 314 explosionRadius, // radius 315 explosionRadius * 0.5f, // coreRadius 316 damage, // maxDamage 317 damage / 2f, // minDamage 318 CollisionClass.PROJECTILE_NO_FF, // collisionClass 319 CollisionClass.GAS_CLOUD, // collisionClassByFighter - using to flag it as from this effect 320 3f, // particleSizeMin 321 3f, // particleSizeRange 322 0.5f, // particleDuration 323 0, // particleCount 324 new Color(255,255,255,0), // particleColor 325 new Color(255,100,100,0) // explosionColor 326 ); 327 spec.setMinEMPDamage(emp * 0.5f); 328 spec.setMaxEMPDamage(emp); 329 330 spec.setDamageType(DamageType.ENERGY); 331 spec.setUseDetailedExplosion(false); 332 spec.setSoundSetId("rift_lightning_explosion"); 333 spec.setSoundVolume(0.5f + 0.5f * mult); 334 335 DamagingProjectileAPI explosion = engine.spawnDamagingExplosion(spec, ship, point); 336 337 //explosion.addDamagedAlready(target); 338 //color = new Color(255,75,75,255); 339 340 // float baseSize = 10f; 341 // 342 // NEParams p = RiftCascadeMineExplosion.createStandardRiftParams( 343 // color, baseSize); 344 // //p.hitGlowSizeMult = 0.5f; 345 // p.noiseMult = 6f; 346 // p.thickness = 25f; 347 // p.fadeOut = 0.5f; 348 // p.spawnHitGlowAt = 1f; 349 // p.additiveBlend = true; 350 // p.blackColor = Color.white; 351 // p.underglow = null; 352 // p.withNegativeParticles = false; 353 // p.withHitGlow = false; 354 // p.fadeIn = 0f; 355 // //p.numRiftsToSpawn = 1; 356 // 357 // RiftCascadeMineExplosion.spawnStandardRift(explosion, p); 358 359 Color color = RiftLightningEffect.RIFT_LIGHTNING_COLOR; 360 color = new Color(255,75,75,255); 361 NEParams p = RiftCascadeMineExplosion.createStandardRiftParams( 362 color, 14f + 6f * mult); 363 p.fadeOut = 0.5f + 0.5f * mult; 364 p.hitGlowSizeMult = 0.6f; 365 p.thickness = 50f; 366 //p.thickness = 25f; 367 368 369 // p.hitGlowSizeMult = 0.5f; 370 // p.thickness = 25f; 371 // p.fadeOut = 0.25f; 372 373 RiftCascadeMineExplosion.spawnStandardRift(explosion, p); 374 } 375 })); 376 377 } 378 379 @Override 380 public boolean shouldAddDescriptionToTooltip(HullSize hullSize, ShipAPI ship, boolean isForModSpec) { 381 return false; 382 } 383 384 @Override 385 public void addPostDescriptionSection(TooltipMakerAPI tooltip, HullSize hullSize, final ShipAPI ship, float width, boolean isForModSpec) { 386 float pad = 3f; 387 float opad = 10f; 388 Color h = Misc.getHighlightColor(); 389 Color bad = Misc.getNegativeHighlightColor(); 390 391 392 tooltip.addPara("Fires rift lightning bolts at locations recently hit by this ship's weapons.", opad); 393 394 tooltip.addPara("The amount of damage and the flux generated depend on the size of the ship the " 395 + "hullmod is installed on. The probability of a bolt being fired increases with the number " 396 + "of hits landed over the previous %s seconds. " 397 + "Bolts are not triggered by weapons dealing soft flux damage, such as beams.", opad, 398 Misc.getHighlightColor(), "" + (int)Math.round(RECENT_HIT_DUR)); 399 400 //tooltip.addSectionHeading("Reload capacity", Alignment.MID, opad); 401 402 if (isForModSpec || (ship == null && !Global.CODEX_TOOLTIP_MODE)) return; 403 404 tooltip.setBgAlpha(0.9f); 405 406 HullSize [] sizes = new HullSize[] {HullSize.FRIGATE, HullSize.DESTROYER, HullSize.CRUISER, HullSize.CAPITAL_SHIP}; 407 408 float damW = 87f; 409 float empW = 87f; 410 float fluxW = 87f; 411 float sizeW = width - damW - empW - fluxW - 10f; 412 tooltip.beginTable(Misc.getBasePlayerColor(), Misc.getDarkPlayerColor(), Misc.getBrightPlayerColor(), 413 20f, true, true, 414 new Object [] {"Ship size", sizeW, "Damage", damW, "EMP", empW, "Flux cost", fluxW}); 415 416 for (HullSize size : sizes) { 417 float damage = getDamage(size); 418 float emp = getEMPDamage(size); 419 float fluxCost = getFluxCost(size); 420 421 Color c = Misc.getGrayColor(); 422 if (size == hullSize || Global.CODEX_TOOLTIP_MODE) { 423 c = Misc.getHighlightColor(); 424 } 425 tooltip.addRow(Alignment.MID, c, Misc.getHullSizeStr(size), 426 Alignment.MID, c, "" + (int) Math.round(damage), 427 Alignment.MID, c, "" + (int) Math.round(emp), 428 Alignment.MID, c, "" + "" + (int) Math.round(fluxCost)); 429 } 430 tooltip.addTable("", 0, opad); 431 432 tooltip.addSpacer(5f); 433 434 addCrewCasualties(tooltip, opad); 435 436// if (Global.CODEX_TOOLTIP_MODE) { 437// return; 438// } 439 } 440 441 442// @Override 443// public String getSModDescriptionParam(int index, HullSize hullSize, ShipAPI ship) { 444// if (index == 0) return "" + (int) Math.round(SMOD_CR_PENALTY * 100f) + "%"; 445// if (index == 1) return "" + (int) Math.round(SMOD_MAINTENANCE_PENALTY) + "%"; 446// return null; 447// } 448// 449// @Override 450// public boolean isSModEffectAPenalty() { 451// return true; 452// } 453} 454 455 456 457 458 459 460 461 462 463 464 465 466 467