001package com.fs.starfarer.api.impl.campaign.abilities; 002 003import java.awt.Color; 004 005import com.fs.starfarer.api.EveryFrameScript; 006import com.fs.starfarer.api.Global; 007import com.fs.starfarer.api.campaign.BattleAPI; 008import com.fs.starfarer.api.campaign.CampaignFleetAPI; 009import com.fs.starfarer.api.campaign.SectorEntityToken.VisibilityLevel; 010import com.fs.starfarer.api.campaign.ai.FleetAIFlags; 011import com.fs.starfarer.api.campaign.ai.ModularFleetAIAPI; 012import com.fs.starfarer.api.campaign.econ.MarketAPI; 013import com.fs.starfarer.api.campaign.rules.MemoryAPI; 014import com.fs.starfarer.api.characters.AbilityPlugin; 015import com.fs.starfarer.api.impl.campaign.CoreReputationPlugin.RepActionEnvelope; 016import com.fs.starfarer.api.impl.campaign.CoreReputationPlugin.RepActions; 017import com.fs.starfarer.api.impl.campaign.ids.Abilities; 018import com.fs.starfarer.api.impl.campaign.ids.MemFlags; 019import com.fs.starfarer.api.impl.campaign.ids.Pings; 020import com.fs.starfarer.api.loading.CampaignPingSpec; 021import com.fs.starfarer.api.ui.LabelAPI; 022import com.fs.starfarer.api.ui.TooltipMakerAPI; 023import com.fs.starfarer.api.util.Misc; 024 025public class InterdictionPulseAbility extends BaseDurationAbility { 026 027 public static class IPReactionScript implements EveryFrameScript { 028 float delay; 029 boolean done; 030 CampaignFleetAPI other; 031 CampaignFleetAPI fleet; 032 float activationDays; 033 /** 034 * fleet is using IP, other is reacting. 035 * @param fleet 036 * @param other 037 * @param activationDays 038 */ 039 public IPReactionScript(CampaignFleetAPI fleet, CampaignFleetAPI other, float activationDays) { 040 this.fleet = fleet; 041 this.other = other; 042 this.activationDays = activationDays; 043 delay = 0.3f + 0.3f * (float) Math.random(); 044 //delay = 0f; 045 } 046 public void advance(float amount) { 047 if (done) return; 048 049 delay -= amount; 050 if (delay > 0) return; 051 052 VisibilityLevel level = fleet.getVisibilityLevelTo(other); 053 if (level == VisibilityLevel.NONE || level == VisibilityLevel.SENSOR_CONTACT) { 054 done = true; 055 return; 056 } 057 058 if (!(other.getAI() instanceof ModularFleetAIAPI)) { 059 done = true; 060 return; 061 } 062 ModularFleetAIAPI ai = (ModularFleetAIAPI) other.getAI(); 063 064 065 float dist = Misc.getDistance(fleet.getLocation(), other.getLocation()); 066 float speed = Math.max(1f, other.getTravelSpeed()); 067 float eta = dist / speed; 068 069 float rushTime = activationDays * Global.getSector().getClock().getSecondsPerDay(); 070 rushTime += 0.5f + 0.5f * (float) Math.random(); 071 072 MemoryAPI mem = other.getMemoryWithoutUpdate(); 073 CampaignFleetAPI pursueTarget = mem.getFleet(FleetAIFlags.PURSUIT_TARGET); 074 075 if (eta < rushTime && pursueTarget == fleet) { 076 done = true; 077 return; 078 } 079 080 float range = InterdictionPulseAbility.getRange(fleet); 081 float getAwayTime = 1f + (range - dist) / speed; 082 AbilityPlugin sb = other.getAbility(Abilities.SENSOR_BURST); 083 if (getAwayTime > rushTime && sb != null && sb.isUsable() && (float) Math.random() > 0.67f) { 084 sb.activate(); 085 done = true; 086 return; 087 } 088 089 //float avoidRange = Math.min(dist, getRange(other)); 090 float avoidRange = getRange(other) + 100f; 091 ai.getNavModule().avoidLocation(fleet.getContainingLocation(), 092 fleet.getLocation(), avoidRange, avoidRange + 50f, activationDays + 0.01f); 093 094 ai.getNavModule().avoidLocation(fleet.getContainingLocation(), 095 //fleet.getLocation(), dist, dist + 50f, activationDays + 0.01f); 096 Misc.getPointAtRadius(fleet.getLocation(), avoidRange * 0.5f), avoidRange, avoidRange * 1.5f + 50f, activationDays + 0.05f); 097 098 done = true; 099 } 100 101 public boolean isDone() { 102 return done; 103 } 104 public boolean runWhilePaused() { 105 return false; 106 } 107 } 108 109 public static final float MAX_EFFECT = 1f; 110 //public static final float RANGE = 1000f; 111 public static final float BASE_RANGE = 500f; 112 public static final float BASE_SECONDS = 6f; 113 public static final float STRENGTH_PER_SECOND = 200f; 114 115 //public static final float CR_COST_MULT = 0.5f; 116 public static final float DETECTABILITY_PERCENT = 100f; 117 118// public String getSpriteName() { 119// return Global.getSettings().getSpriteName("abilities", Abilities.EMERGENCY_BURN); 120// } 121 122 123 public static float getRange(CampaignFleetAPI fleet) { 124 float max = Global.getSettings().getMaxSensorRange(); 125 return Math.min(max, BASE_RANGE + fleet.getSensorRangeMod().computeEffective(fleet.getSensorStrength()) / 2f); 126 } 127 128 @Override 129 protected String getActivationText() { 130 //return Misc.ucFirst(spec.getName().toLowerCase()); 131 return "Interdiction pulse"; 132 } 133 134 135 protected Boolean primed = null; 136 protected Float elapsed = null; 137 protected Integer numFired = null; 138 139 @Override 140 protected void activateImpl() { 141 CampaignFleetAPI fleet = getFleet(); 142 if (fleet == null) return; 143 144 Global.getSector().addPing(fleet, Pings.INTERDICT); 145 146 float range = getRange(fleet); 147 for (CampaignFleetAPI other : fleet.getContainingLocation().getFleets()) { 148 if (other == fleet) continue; 149 150 float dist = Misc.getDistance(fleet.getLocation(), other.getLocation()); 151 if (dist > range + 500f) continue; 152 153 other.addScript(new IPReactionScript(fleet, other, getActivationDays())); 154 } 155 156 primed = true; 157 158 } 159 160 protected void showRangePing(float amount) { 161 CampaignFleetAPI fleet = getFleet(); 162 if (fleet == null) return; 163 164 VisibilityLevel vis = fleet.getVisibilityLevelToPlayerFleet(); 165 if (vis == VisibilityLevel.NONE || vis == VisibilityLevel.SENSOR_CONTACT) return; 166 167 168 boolean fire = false; 169 if (elapsed == null) { 170 elapsed = 0f; 171 numFired = 0; 172 fire = true; 173 } 174 elapsed += amount; 175 if (elapsed > 0.5f && numFired < 4) { 176 elapsed -= 0.5f; 177 fire = true; 178 } 179 180 if (fire) { 181 numFired++; 182 183 float range = getRange(fleet); 184 CampaignPingSpec custom = new CampaignPingSpec(); 185 custom.setUseFactionColor(true); 186 custom.setWidth(7); 187 custom.setMinRange(range - 100f); 188 custom.setRange(200); 189 custom.setDuration(2f); 190 custom.setAlphaMult(0.25f); 191 custom.setInFraction(0.2f); 192 custom.setNum(1); 193 194 Global.getSector().addPing(fleet, custom); 195 } 196 197 } 198 199 @Override 200 protected void applyEffect(float amount, float level) { 201 CampaignFleetAPI fleet = getFleet(); 202 if (fleet == null) return; 203 204 fleet.getStats().getDetectedRangeMod().modifyPercent(getModId(), DETECTABILITY_PERCENT * level, "Interdiction pulse"); 205 206 //System.out.println("Level: " + level); 207 208 if (level > 0 && level < 1 && amount > 0) { 209 showRangePing(amount); 210// float activateSeconds = getActivationDays() * Global.getSector().getClock().getSecondsPerDay(); 211// float speed = fleet.getVelocity().length(); 212// float acc = Math.max(speed, 200f)/activateSeconds + fleet.getAcceleration(); 213// float ds = acc * amount; 214// if (ds > speed) ds = speed; 215// Vector2f dv = Misc.getUnitVectorAtDegreeAngle(Misc.getAngleInDegrees(fleet.getVelocity())); 216// dv.scale(ds); 217// fleet.setVelocity(fleet.getVelocity().x - dv.x, fleet.getVelocity().y - dv.y); 218 fleet.goSlowOneFrame(); 219 return; 220 } 221 222 float range = getRange(fleet); 223 224 boolean playedHit = !(entity.isInCurrentLocation() && entity.isVisibleToPlayerFleet()); 225 if (level == 1 && primed != null) { 226 227 if (entity.isInCurrentLocation()) { 228 Global.getSector().getMemoryWithoutUpdate().set(MemFlags.GLOBAL_INTERDICTION_PULSE_JUST_USED_IN_CURRENT_LOCATION, true, 0.1f); 229 } 230 fleet.getMemoryWithoutUpdate().set(MemFlags.JUST_DID_INTERDICTION_PULSE, true, 0.1f); 231 232 CampaignPingSpec custom = new CampaignPingSpec(); 233 custom.setUseFactionColor(true); 234 custom.setWidth(15); 235 custom.setRange(range * 1.3f); 236 custom.setDuration(0.5f); 237 custom.setAlphaMult(1f); 238 custom.setInFraction(0.1f); 239 custom.setNum(1); 240 Global.getSector().addPing(fleet, custom); 241 242 243 for (CampaignFleetAPI other : fleet.getContainingLocation().getFleets()) { 244 if (other == fleet) continue; 245 if (other.getFaction() == fleet.getFaction()) continue; 246 if (other.isInHyperspaceTransition()) continue; 247 248 float dist = Misc.getDistance(fleet.getLocation(), other.getLocation()); 249 if (dist > range) continue; 250 251 252 float interdictSeconds = getInterdictSeconds(fleet, other); 253 if (interdictSeconds > 0 && interdictSeconds < 1f) interdictSeconds = 1f; 254 255 VisibilityLevel vis = other.getVisibilityLevelToPlayerFleet(); 256 if (vis == VisibilityLevel.COMPOSITION_AND_FACTION_DETAILS || 257 vis == VisibilityLevel.COMPOSITION_DETAILS || 258 (vis == VisibilityLevel.SENSOR_CONTACT && fleet.isPlayerFleet())) { 259 if (interdictSeconds <= 0) { 260 other.addFloatingText("Interdict avoided!" , fleet.getFaction().getBaseUIColor(), 1f, true); 261 continue; 262 } else { 263 other.addFloatingText("Interdict! (" + (int) Math.round(interdictSeconds) + "s)" , fleet.getFaction().getBaseUIColor(), 1f, true); 264 } 265 } 266 267 float interdictDays = interdictSeconds / Global.getSector().getClock().getSecondsPerDay(); 268 269 for (AbilityPlugin ability : other.getAbilities().values()) { 270 if (!ability.getSpec().hasTag(Abilities.TAG_BURN + "+") && 271 !ability.getSpec().hasTag(Abilities.TAG_DISABLED_BY_INTERDICT) && 272 !ability.getId().equals(Abilities.INTERDICTION_PULSE)) continue; 273 274 float origCooldown = ability.getCooldownLeft(); 275 float extra = 0; 276 if (ability.isActiveOrInProgress()) { 277 extra += ability.getSpec().getDeactivationCooldown() * ability.getProgressFraction(); 278 ability.deactivate(); 279 280 } 281 282 if (!ability.getSpec().hasTag(Abilities.TAG_BURN + "+")) continue; 283 284 float cooldown = interdictDays; 285 //cooldown = Math.max(cooldown, origCooldown); 286 cooldown += origCooldown; 287 cooldown += extra; 288 float max = Math.max(ability.getSpec().getDeactivationCooldown(), 2f); 289 if (cooldown > max) cooldown = max; 290 ability.setCooldownLeft(cooldown); 291 } 292 293 if (fleet.isPlayerFleet() && other.knowsWhoPlayerIs() && fleet.getFaction() != other.getFaction()) { 294 Global.getSector().adjustPlayerReputation( 295 new RepActionEnvelope(RepActions.INTERDICTED, null, null, false), 296 other.getFaction().getId()); 297 } 298 299 if (!playedHit) { 300 Global.getSoundPlayer().playSound("world_interdict_hit", 1f, 1f, other.getLocation(), other.getVelocity()); 301 //playedHit = true; 302 } 303 } 304 305 primed = null; 306 elapsed = null; 307 numFired = null; 308 } 309 310 } 311 312 public static float getInterdictSeconds(CampaignFleetAPI fleet, CampaignFleetAPI other) { 313 float offense = fleet.getSensorRangeMod().computeEffective(fleet.getSensorStrength()); 314 float defense = other.getSensorRangeMod().computeEffective(other.getSensorStrength()); 315 float diff = offense - defense; 316 317 float extra = diff / STRENGTH_PER_SECOND; 318 319 float total = BASE_SECONDS + extra; 320 if (total < 0f) total = 0f; 321 return total;// / Global.getSector().getClock().getSecondsPerDay(); 322 } 323 324 325// public static float getEffectMagnitude(CampaignFleetAPI fleet, CampaignFleetAPI other) { 326// float burn = Misc.getBurnLevelForSpeed(other.getVelocity().length()); 327// 328// Vector2f velDir = Misc.normalise(new Vector2f(other.getVelocity())); 329// Vector2f toFleet = Misc.normalise(Vector2f.sub(fleet.getLocation(), other.getLocation(), new Vector2f())); 330// float dot = Vector2f.dot(velDir, toFleet); 331// if (dot <= 0.05f || burn <= 1f) return 0f; 332// 333// float effect = dot; 334// if (effect < 0) effect = 0; 335// if (effect > 1) effect = 1; 336// 337// //effect *= Math.min(1f, burn / 10f); 338// 339// //return effect; 340// return Math.max(0.1f, effect); 341// } 342 343 @Override 344 protected void deactivateImpl() { 345 cleanupImpl(); 346 } 347 348 @Override 349 protected void cleanupImpl() { 350 CampaignFleetAPI fleet = getFleet(); 351 if (fleet == null) return; 352 353 fleet.getStats().getDetectedRangeMod().unmodify(getModId()); 354 //fleet.getStats().getSensorRangeMod().unmodify(getModId()); 355 //fleet.getStats().getFleetwideMaxBurnMod().unmodify(getModId()); 356 //fleet.getStats().getAccelerationMult().unmodify(getModId()); 357 //fleet.getCommanderStats().getDynamic().getStat(Stats.NAVIGATION_PENALTY_MULT).unmodify(getModId()); 358 359 primed = null; 360 } 361 362 363 @Override 364 public boolean isUsable() { 365 return super.isUsable() && 366 getFleet() != null;// && 367 //getNonReadyShips().isEmpty(); 368 } 369 370// protected List<FleetMemberAPI> getNonReadyShips() { 371// List<FleetMemberAPI> result = new ArrayList<FleetMemberAPI>(); 372// CampaignFleetAPI fleet = getFleet(); 373// if (fleet == null) return result; 374// 375// float crCostFleetMult = fleet.getStats().getDynamic().getValue(Stats.EMERGENCY_BURN_CR_MULT); 376// for (FleetMemberAPI member : fleet.getFleetData().getMembersListCopy()) { 377// //if (member.isMothballed()) continue; 378// float crLoss = member.getDeployCost() * CR_COST_MULT * crCostFleetMult; 379// if (Math.round(member.getRepairTracker().getCR() * 100) < Math.round(crLoss * 100)) { 380// result.add(member); 381// } 382// } 383// return result; 384// } 385 386// protected float computeSupplyCost() { 387// CampaignFleetAPI fleet = getFleet(); 388// if (fleet == null) return 0f; 389// 390// float crCostFleetMult = fleet.getStats().getDynamic().getValue(Stats.EMERGENCY_BURN_CR_MULT); 391// 392// float cost = 0f; 393// for (FleetMemberAPI member : fleet.getFleetData().getMembersListCopy()) { 394// cost += member.getDeploymentPointsCost() * CR_COST_MULT * crCostFleetMult; 395// } 396// return cost; 397// } 398 399 400 @Override 401 public void createTooltip(TooltipMakerAPI tooltip, boolean expanded) { 402 CampaignFleetAPI fleet = getFleet(); 403 if (fleet == null) return; 404 405 Color gray = Misc.getGrayColor(); 406 Color highlight = Misc.getHighlightColor(); 407 Color fuel = Global.getSettings().getColor("progressBarFuelColor"); 408 Color bad = Misc.getNegativeHighlightColor(); 409 410 if (!Global.CODEX_TOOLTIP_MODE) { 411 LabelAPI title = tooltip.addTitle("Interdiction Pulse"); 412 } else { 413 tooltip.addSpacer(-10f); 414 } 415 416 float pad = 10f; 417 418 int range = (int) getRange(fleet); 419 420 421 tooltip.addPara("Slows* the fleet and uses its active sensor network to charge and release a powerful energy pulse that " + 422 "can disrupt the drive fields of nearby fleets.", pad); 423 424 Color c = Misc.getTooltipTitleAndLightHighlightColor(); 425 Color hc = highlight; 426 if (Global.CODEX_TOOLTIP_MODE) hc = Misc.getBasePlayerColor(); 427 tooltip.addPara("The disruption interrupts any movement-related abilities (such as %s or %s) " + 428 "and prevents their use for some time afterwards. Also interrupts charging interdiction pulses.", pad, 429 hc, "Sustained Burn", "Emergency Burn"); 430 431 tooltip.addPara("The disruption lasts for %s seconds, modified by %s second for " + 432 "every %s points of difference in the fleets' sensor strengths.", pad, highlight, 433 "" + (int) BASE_SECONDS, 434 "" + (int) 1, 435 "" + (int) STRENGTH_PER_SECOND); 436 437 tooltip.addPara("Base range of %s* units, increased by half your fleet's sensor strength, " + 438 "for a total of %s units. While the pulse is charging, the range at which the fleet can be detected will " + 439 "gradually increase by up to %s.", pad, highlight, 440 "" + (int) BASE_RANGE, 441 "" + range, 442 "" + (int) DETECTABILITY_PERCENT + "%"); 443 444 tooltip.addPara("A successful interdict is considered a hostile act, though not on the same level as " + 445 "open warfare.", pad); 446 447 tooltip.addPara("*2000 units = 1 map grid cell", gray, pad); 448 tooltip.addPara("*A fleet is considered slow-moving at a burn level of half that of its slowest ship.", gray, pad); 449 addIncompatibleToTooltip(tooltip, expanded); 450 } 451 452 public boolean hasTooltip() { 453 return true; 454 } 455 456 457 @Override 458 public void fleetLeftBattle(BattleAPI battle, boolean engagedInHostilities) { 459 if (engagedInHostilities) { 460 deactivate(); 461 } 462 } 463 464 @Override 465 public void fleetOpenedMarket(MarketAPI market) { 466 deactivate(); 467 } 468 469} 470 471 472 473 474