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