001package com.fs.starfarer.api.impl.campaign.abilities; 002 003import java.util.Random; 004 005import java.awt.Color; 006 007import org.lwjgl.util.vector.Vector2f; 008 009import com.fs.starfarer.api.Global; 010import com.fs.starfarer.api.campaign.CampaignFleetAPI; 011import com.fs.starfarer.api.campaign.JumpPointAPI; 012import com.fs.starfarer.api.campaign.SectorEntityToken; 013import com.fs.starfarer.api.campaign.SectorEntityToken.VisibilityLevel; 014import com.fs.starfarer.api.campaign.StarSystemAPI; 015import com.fs.starfarer.api.characters.FullName; 016import com.fs.starfarer.api.impl.campaign.econ.impl.MilitaryBase; 017import com.fs.starfarer.api.impl.campaign.fleets.FleetFactory.PatrolType; 018import com.fs.starfarer.api.impl.campaign.fleets.PirateFleetManager; 019import com.fs.starfarer.api.impl.campaign.fleets.RouteManager; 020import com.fs.starfarer.api.impl.campaign.fleets.RouteManager.OptionalFleetData; 021import com.fs.starfarer.api.impl.campaign.fleets.RouteManager.RouteData; 022import com.fs.starfarer.api.impl.campaign.fleets.RouteManager.RouteFleetSpawner; 023import com.fs.starfarer.api.impl.campaign.fleets.RouteManager.RouteSegment; 024import com.fs.starfarer.api.impl.campaign.ids.Factions; 025import com.fs.starfarer.api.impl.campaign.ids.FleetTypes; 026import com.fs.starfarer.api.impl.campaign.ids.Pings; 027import com.fs.starfarer.api.impl.campaign.ids.Tags; 028import com.fs.starfarer.api.impl.campaign.procgen.themes.RuinsFleetRouteManager; 029import com.fs.starfarer.api.impl.campaign.procgen.themes.SalvageSpecialAssigner; 030import com.fs.starfarer.api.ui.LabelAPI; 031import com.fs.starfarer.api.ui.TooltipMakerAPI; 032import com.fs.starfarer.api.util.DelayedActionScript; 033import com.fs.starfarer.api.util.Misc; 034import com.fs.starfarer.api.util.TimeoutTracker; 035import com.fs.starfarer.api.util.WeightedRandomPicker; 036 037public class DistressCallAbility extends BaseDurationAbility implements RouteFleetSpawner { 038 039 public static final float NEARBY_USE_TIMEOUT_DAYS = 20f; 040 public static final float NEARBY_USE_RADIUS_LY = 5f; 041 042 public static final float DAYS_TO_TRACK_USAGE = 365f; 043 044 public static enum DistressCallOutcome { 045 NOTHING, 046 HELP, 047 PIRATES, 048 } 049 050 public static class DistressResponseData { 051 public DistressCallOutcome outcome; 052 public JumpPointAPI inner; 053 public JumpPointAPI outer; 054 } 055 056 057 public static class AbilityUseData { 058 public long timestamp; 059 public Vector2f location; 060 public AbilityUseData(long timestamp, Vector2f location) { 061 this.timestamp = timestamp; 062 this.location = location; 063 } 064 065 } 066 067 protected boolean performed = false; 068 protected int numTimesUsed = 0; 069 protected long lastUsed = 0; 070 071 protected TimeoutTracker<AbilityUseData> uses = new TimeoutTracker<AbilityUseData>(); 072 073 protected Object readResolve() { 074 super.readResolve(); 075 if (uses == null) { 076 uses = new TimeoutTracker<AbilityUseData>(); 077 } 078 return this; 079 } 080 081 @Override 082 protected void activateImpl() { 083 if (entity.isInCurrentLocation()) { 084 VisibilityLevel level = entity.getVisibilityLevelToPlayerFleet(); 085 if (level != VisibilityLevel.NONE) { 086 Global.getSector().addPing(entity, Pings.DISTRESS_CALL); 087 } 088 089 performed = false; 090 } 091 092 } 093 094 @Override 095 protected void applyEffect(float amount, float level) { 096 CampaignFleetAPI fleet = getFleet(); 097 if (fleet == null) return; 098 099 if (!performed) { 100 if (wasUsedNearby(NEARBY_USE_TIMEOUT_DAYS, fleet.getLocationInHyperspace(), NEARBY_USE_RADIUS_LY)) { 101 performed = true; 102 return; 103 } 104 105 WeightedRandomPicker<DistressCallOutcome> picker = new WeightedRandomPicker<DistressCallOutcome>(); 106 picker.add(DistressCallOutcome.HELP, 10f); 107 if (numTimesUsed > 2) { 108 float uses = getNumUsesInLastPeriod(); 109 float pirates = 10f + uses * 2f; 110 picker.add(DistressCallOutcome.PIRATES, pirates); 111 112 float nothing = 10f + uses * 2f; 113 picker.add(DistressCallOutcome.NOTHING, nothing); 114 } 115 116 DistressCallOutcome outcome = picker.pick(); 117 //outcome = DistressCallOutcome.HELP; 118 119 if (outcome != DistressCallOutcome.NOTHING) { 120 float delay = 10f + 10f * (float) Math.random(); 121 if (numTimesUsed == 0) { 122 delay = 1f + 2f * (float) Math.random(); 123 } 124 //delay = 0f; 125 addResponseScript(delay, outcome); 126 } 127 128 numTimesUsed++; 129 lastUsed = Global.getSector().getClock().getTimestamp(); 130 performed = true; 131 132 AbilityUseData data = new AbilityUseData(lastUsed, fleet.getLocationInHyperspace()); 133 uses.add(data, DAYS_TO_TRACK_USAGE); 134 } 135 } 136 137 public boolean wasUsedNearby(float withinDays, Vector2f locInHyper, float withinRangeLY) { 138 for (AbilityUseData data : uses.getItems()) { 139 float daysSinceUse = Global.getSector().getClock().getElapsedDaysSince(data.timestamp); 140 if (daysSinceUse <= withinDays) { 141 float range = Misc.getDistanceLY(locInHyper, data.location); 142 if (range <= withinRangeLY) return true; 143 } 144 } 145 return false; 146 } 147 148 149 @Override 150 public void advance(float amount) { 151 super.advance(amount); 152 153 float days = Global.getSector().getClock().convertToDays(amount); 154 uses.advance(days); 155 } 156 157 public TimeoutTracker<AbilityUseData> getUses() { 158 return uses; 159 } 160 161 public int getNumUsesInLastPeriod() { 162 return uses.getItems().size(); 163 } 164 165 protected void addResponseScript(float delayDays, DistressCallOutcome outcome) { 166 final CampaignFleetAPI player = getFleet(); 167 if (player == null) return; 168 if (!(player.getContainingLocation() instanceof StarSystemAPI)) return; 169 170 final StarSystemAPI system = (StarSystemAPI) player.getContainingLocation(); 171 172 final JumpPointAPI inner = Misc.getDistressJumpPoint(system); 173 if (inner == null) return; 174 175 JumpPointAPI outerTemp = null; 176 if (inner.getDestinations().size() >= 1) { 177 SectorEntityToken test = inner.getDestinations().get(0).getDestination(); 178 if (test instanceof JumpPointAPI) { 179 outerTemp = (JumpPointAPI) test; 180 } 181 } 182 final JumpPointAPI outer = outerTemp; 183 if (outer == null) return; 184 185 186 if (outcome == DistressCallOutcome.HELP) { 187 addHelpScript(delayDays, system, inner, outer); 188 } else if (outcome == DistressCallOutcome.PIRATES) { 189 addPiratesScript(delayDays, system, inner, outer); 190 } 191 192 } 193 194 protected void addPiratesScript(float delayDays, 195 final StarSystemAPI system, 196 final JumpPointAPI inner, 197 final JumpPointAPI outer) { 198 Global.getSector().addScript(new DelayedActionScript(delayDays) { 199 @Override 200 public void doAction() { 201 CampaignFleetAPI player = Global.getSector().getPlayerFleet(); 202 if (player == null) return; 203 204 int numPirates = new Random().nextInt(3) + 1; 205 for (int i = 0; i < numPirates; i++) { 206 DistressResponseData data = new DistressResponseData(); 207 data.outcome = DistressCallOutcome.PIRATES; 208 data.inner = inner; 209 data.outer = outer; 210 211 OptionalFleetData extra = new OptionalFleetData(); 212 extra.factionId = Factions.PIRATES; 213 214 RouteData route = RouteManager.getInstance().addRoute("dca_" + getId(), null, 215 Misc.genRandomSeed(), extra, DistressCallAbility.this, data); 216 float waitDays = 15f + (float) Math.random() * 10f; 217 route.addSegment(new RouteSegment(waitDays, inner)); 218 } 219 } 220 }); 221 } 222 223 protected void addHelpScript(float delayDays, 224 final StarSystemAPI system, 225 final JumpPointAPI inner, 226 final JumpPointAPI outer) { 227 Global.getSector().addScript(new DelayedActionScript(delayDays) { 228 @Override 229 public void doAction() { 230 DistressResponseData data = new DistressResponseData(); 231 data.outcome = DistressCallOutcome.HELP; 232 data.inner = inner; 233 data.outer = outer; 234 235 RouteData route = RouteManager.getInstance().addRoute("dca_" + getId(), null, 236 Misc.genRandomSeed(), null, DistressCallAbility.this, data); 237 float waitDays = 15f + (float) Math.random() * 10f; 238 route.addSegment(new RouteSegment(waitDays, inner)); 239 } 240 }); 241 } 242 243 244 245 246 public boolean isUsable() { 247 if (!super.isUsable()) return false; 248 if (getFleet() == null) return false; 249 250 CampaignFleetAPI fleet = getFleet(); 251 if (fleet.isInHyperspace() || fleet.isInHyperspaceTransition()) return false; 252 253 if (fleet.getContainingLocation() != null && fleet.getContainingLocation().hasTag(Tags.SYSTEM_ABYSSAL)) { 254 return false; 255 } 256 257 return true; 258 } 259 260 261 @Override 262 protected void deactivateImpl() { 263 cleanupImpl(); 264 } 265 266 @Override 267 protected void cleanupImpl() { 268 CampaignFleetAPI fleet = getFleet(); 269 if (fleet == null) return; 270 } 271 272 273 @Override 274 public void createTooltip(TooltipMakerAPI tooltip, boolean expanded) { 275 276 CampaignFleetAPI fleet = getFleet(); 277 if (fleet == null) return; 278 279 Color gray = Misc.getGrayColor(); 280 Color highlight = Misc.getHighlightColor(); 281 Color bad = Misc.getNegativeHighlightColor(); 282 283 if (!Global.CODEX_TOOLTIP_MODE) { 284 LabelAPI title = tooltip.addTitle(spec.getName()); 285 } else { 286 tooltip.addSpacer(-10f); 287 } 288 289 float pad = 10f; 290 291 tooltip.addPara("May be used by a stranded fleet to punch a distress signal through to hyperspace, " + 292 "asking nearby fleets to bring aid in the form of fuel and supplies. " + 293 "Help may take many days to arrive, if it arrives at all, and taking advantage " + 294 "of it will result in a progressively higher reduction in standing with the responders.", pad); 295 296 tooltip.addPara("By long-standing convention, the fleet in distress is expected to meet any responders at the " + 297 "innermost jump-point inside a star system.", pad, highlight, 298 "innermost jump-point"); 299 300 tooltip.addPara("The signal is non-directional and carries no data, and is therefore not useful for " + 301 "calling for help in a tactical situation.", pad); 302 303 if (!Global.CODEX_TOOLTIP_MODE) { 304 if (fleet.isInHyperspace()) { 305 tooltip.addPara("Can not be used in hyperspace.", bad, pad); 306 } 307 if (fleet.getContainingLocation() != null && fleet.getContainingLocation().hasTag(Tags.SYSTEM_ABYSSAL)) { 308 tooltip.addPara("Can not be used in star systems deep within abyssal hyperspace.", bad, pad); 309 } 310 } 311 312 addIncompatibleToTooltip(tooltip, expanded); 313 314 } 315 316 public boolean hasTooltip() { 317 return true; 318 } 319 320 321 public CampaignFleetAPI spawnFleet(RouteData route) { 322 323 DistressResponseData data = (DistressResponseData) route.getCustom(); 324 325 CampaignFleetAPI player = Global.getSector().getPlayerFleet(); 326 if (player == null) return null; 327 328 if (data.outcome == DistressCallOutcome.HELP) { 329 WeightedRandomPicker<String> factions = SalvageSpecialAssigner.getNearbyFactions( 330 null, data.inner, 331 10f, 10f, 0f); 332 333 String faction = factions.pick(); 334// faction = Factions.HEGEMONY; 335// faction = Factions.PIRATES; 336 if (faction == null) return null; 337 338 //int fuelNeeded = DistressResponse.getNeededFuel(player); 339 340 CampaignFleetAPI fleet = null; 341 if (Factions.INDEPENDENT.equals(faction)) { 342 WeightedRandomPicker<String> typePicker = new WeightedRandomPicker<String>(); 343// typePicker.add(FleetTypes.SCAVENGER_SMALL, 5f); // too little fuel to bother 344 345 //if (fuelNeeded < 750) { 346 typePicker.add(FleetTypes.SCAVENGER_MEDIUM, 10f); // 500+ fuel 347 typePicker.add(FleetTypes.SCAVENGER_LARGE, 5f); // 1000+ fuel 348 String type = typePicker.pick(); 349 350 fleet = RuinsFleetRouteManager.createScavenger( 351 type, data.inner.getLocationInHyperspace(), 352 route, null, false, null); 353 } else { 354 WeightedRandomPicker<PatrolType> picker = new WeightedRandomPicker<PatrolType>(); 355// picker.add(PatrolType.FAST, 5f); 356// picker.add(PatrolType.COMBAT, 10f); 357 picker.add(PatrolType.HEAVY, 5f); 358 PatrolType type = picker.pick(); 359 360 fleet = MilitaryBase.createPatrol(type, 15f, faction, route, null, data.inner.getLocationInHyperspace(), route.getRandom()); 361 //fleet = PatrolFleetManager.createPatrolFleet(type, null, faction, data.inner.getLocationInHyperspace(), 0f); 362 } 363 if (fleet == null) return null; 364 365 if (Misc.getSourceMarket(fleet) == null) return null; 366 367 368 if (numTimesUsed == 1) { 369 FullName name = new FullName("Mel", "Greenish", fleet.getCommander().getGender()); 370 fleet.getCommander().setName(name); 371 fleet.getFlagship().setShipName("IS In All Circumstances"); 372 } 373 374 Misc.makeImportant(fleet, "distressResponse", 30f); 375 fleet.getMemoryWithoutUpdate().set("$distressResponse", true); 376 377 Global.getSector().getHyperspace().addEntity(fleet); 378 379 if (!player.isInHyperspace() && 380 (Global.getSector().getHyperspace().getDaysSinceLastPlayerVisit() > 5 || 381 player.getCargo().getFuel() <= 0)) { 382 383 Vector2f loc = data.outer.getLocation(); 384 fleet.setLocation(loc.x, loc.y + fleet.getRadius() + 100f); 385 } else { 386 float dir = (float) Math.random() * 360f; 387 if (player.isInHyperspace()) { 388 dir = Misc.getAngleInDegrees(player.getLocation(), data.inner.getLocationInHyperspace()); 389 dir += (float) Math.random() * 120f - 60f; 390 } 391 Vector2f loc = Misc.getUnitVectorAtDegreeAngle(dir); 392 loc.scale(3000f + 1000f * (float) Math.random()); 393 Vector2f.add(data.inner.getLocationInHyperspace(), loc, loc); 394 fleet.setLocation(loc.x, loc.y + fleet.getRadius() + 100f); 395 } 396 397 fleet.addScript(new DistressCallResponseAssignmentAI(fleet, data.inner.getStarSystem(), 398 data.inner, data.outer)); 399 400 return fleet; 401 } else if (data.outcome == DistressCallOutcome.PIRATES) { 402 int points = 5 + new Random().nextInt(15); 403 404 CampaignFleetAPI fleet = PirateFleetManager.createPirateFleet(points, route, data.inner.getLocationInHyperspace()); 405 if (fleet == null) return null; 406 if (Misc.getSourceMarket(fleet) == null) return null; 407 408 Global.getSector().getHyperspace().addEntity(fleet); 409 410 if (!player.isInHyperspace() && 411 (Global.getSector().getHyperspace().getDaysSinceLastPlayerVisit() > 5 || 412 player.getCargo().getFuel() <= 0)) { 413 414 Vector2f loc = data.outer.getLocation(); 415 fleet.setLocation(loc.x, loc.y + fleet.getRadius() + 100f); 416 } else { 417 float dir = (float) Math.random() * 360f; 418 if (player.isInHyperspace()) { 419 dir = Misc.getAngleInDegrees(player.getLocation(), data.inner.getLocationInHyperspace()); 420 dir += (float) Math.random() * 120f - 60f; 421 } 422 Vector2f loc = Misc.getUnitVectorAtDegreeAngle(dir); 423 loc.scale(3000f + 1000f * (float) Math.random()); 424 Vector2f.add(data.inner.getLocationInHyperspace(), loc, loc); 425 fleet.setLocation(loc.x, loc.y + fleet.getRadius() + 100f); 426 } 427 428 fleet.addScript(new DistressCallResponsePirateAssignmentAI(fleet, data.inner.getStarSystem(), data.inner, data.outer)); 429 430 return fleet; 431 } 432 433 434 return null; 435 } 436 437 438 public void reportAboutToBeDespawnedByRouteManager(RouteData route) { 439 // don't respawn since the assignment AI is not set up to handle it well, it'll 440 // just basically start over 441 route.expire(); 442 } 443 444 public boolean shouldCancelRouteAfterDelayCheck(RouteData route) { 445 return false; 446 } 447 448 public boolean shouldRepeat(RouteData route) { 449 return false; 450 } 451 452} 453 454 455 456 457