001package com.fs.starfarer.api.impl.campaign; 002 003import java.util.HashSet; 004import java.util.List; 005import java.util.Random; 006import java.util.Set; 007 008import com.fs.starfarer.api.Global; 009import com.fs.starfarer.api.campaign.CampaignEngineLayers; 010import com.fs.starfarer.api.campaign.CargoAPI; 011import com.fs.starfarer.api.campaign.CustomCampaignEntityAPI; 012import com.fs.starfarer.api.campaign.FactionAPI; 013import com.fs.starfarer.api.campaign.SectorEntityToken; 014import com.fs.starfarer.api.campaign.SectorEntityToken.VisibilityLevel; 015import com.fs.starfarer.api.combat.ShipAPI.HullSize; 016import com.fs.starfarer.api.combat.ViewportAPI; 017import com.fs.starfarer.api.fleet.FleetMemberAPI; 018import com.fs.starfarer.api.fleet.FleetMemberType; 019import com.fs.starfarer.api.graphics.SpriteAPI; 020import com.fs.starfarer.api.impl.campaign.ids.Drops; 021import com.fs.starfarer.api.impl.campaign.ids.ShipRoles; 022import com.fs.starfarer.api.impl.campaign.procgen.DropGroupRow; 023import com.fs.starfarer.api.impl.campaign.procgen.SalvageEntityGenDataSpec.DropData; 024import com.fs.starfarer.api.impl.campaign.rulecmd.salvage.special.BaseSalvageSpecial; 025import com.fs.starfarer.api.impl.campaign.rulecmd.salvage.special.ShipRecoverySpecial.PerShipData; 026import com.fs.starfarer.api.impl.campaign.rulecmd.salvage.special.ShipRecoverySpecial.ShipCondition; 027import com.fs.starfarer.api.ui.TooltipMakerAPI; 028import com.fs.starfarer.api.util.Misc; 029import com.fs.starfarer.api.util.WeightedRandomPicker; 030 031public class DerelictShipEntityPlugin extends BaseCustomEntityPlugin { 032 033 public static float DEFAULT_SMOD_PROB = 0.05f; 034 035 public static enum DerelictType { 036 SMALL, 037 MEDIUM, 038 LARGE, 039 CIVILIAN 040 } 041 042 public static float getDefaultSModProb() { 043 return DEFAULT_SMOD_PROB; 044 } 045 046 public static DerelictShipData createHull(String hullId, Random random, float sModProb) { 047 List<String> list = Global.getSettings().getHullIdToVariantListMap().get(hullId); 048 WeightedRandomPicker<String> picker = new WeightedRandomPicker<String>(random); 049 picker.addAll(list); 050 String variantId = picker.pick(); 051 if (variantId == null) { 052 return null; 053 } 054 return createVariant(variantId, random, sModProb); 055 } 056 public static DerelictShipData createVariant(String variantId, Random random, float sModProb) { 057 ShipCondition condition = pickDerelictCondition(random); 058 PerShipData ship = new PerShipData(variantId, condition, sModProb); 059 return new DerelictShipData(ship, true); 060 } 061 062 public static DerelictShipData createRandom(String factionId, DerelictType type, Random random) { 063 return createRandom(factionId, type, random, 0f); 064 } 065 public static DerelictShipData createRandom(String factionId, DerelictType type, Random random, float sModProb) { 066 if (random == null) random = new Random(); 067 if (type == null) type = pickDerelictType(random); 068 String variantId = null; 069 switch (type) { 070 case CIVILIAN: variantId = pickCivilianVariantId(factionId, random); break; 071 case LARGE: variantId = pickLargeVariantId(factionId, random); break; 072 case MEDIUM: variantId = pickMediumVariantId(factionId, random); break; 073 case SMALL: variantId = pickSmallVariantId(factionId, random); break; 074 } 075 076 if (variantId == null) return null; 077 078 ShipCondition condition = pickDerelictCondition(random); 079 080 PerShipData ship = new PerShipData(variantId, condition, sModProb); 081 082 return new DerelictShipData(ship, true); 083 } 084 085 public static DerelictType pickDerelictType(Random random) { 086 if (random == null) random = new Random(); 087 WeightedRandomPicker<DerelictType> picker = new WeightedRandomPicker<DerelictType>(random); 088 089 picker.add(DerelictType.CIVILIAN, 10f); 090 picker.add(DerelictType.LARGE, 5f); 091 picker.add(DerelictType.MEDIUM, 10f); 092 picker.add(DerelictType.SMALL, 20f); 093 094 return picker.pick(); 095 } 096 public static ShipCondition pickDerelictCondition(Random random) { 097 if (random == null) random = new Random(); 098 WeightedRandomPicker<ShipCondition> picker = new WeightedRandomPicker<ShipCondition>(random); 099 100 picker.add(ShipCondition.WRECKED, 10f); 101 picker.add(ShipCondition.BATTERED, 10f); 102 picker.add(ShipCondition.AVERAGE, 7f); 103 picker.add(ShipCondition.GOOD, 5f); 104 picker.add(ShipCondition.PRISTINE, 1f); 105 106 return picker.pick(); 107 } 108 109 public static ShipCondition pickBadCondition(Random random) { 110 if (random == null) random = new Random(); 111 WeightedRandomPicker<ShipCondition> picker = new WeightedRandomPicker<ShipCondition>(random); 112 113 picker.add(ShipCondition.WRECKED, 10f); 114 picker.add(ShipCondition.BATTERED, 10f); 115 picker.add(ShipCondition.AVERAGE, 3f); 116 117 return picker.pick(); 118 } 119 120 121 public static String pickCivilianVariantId(String factionId, Random random) { 122 String variantId = pickVariant(factionId, random, 123 ShipRoles.CIV_RANDOM, 7f, // ox or crig 124 ShipRoles.FREIGHTER_SMALL, 10f, 125 ShipRoles.FREIGHTER_MEDIUM, 3f, 126 ShipRoles.FREIGHTER_LARGE, 1f, 127 ShipRoles.LINER_SMALL, 10f, 128 ShipRoles.LINER_MEDIUM, 3f, 129 ShipRoles.LINER_LARGE, 1f, 130 ShipRoles.TANKER_SMALL, 10f, 131 ShipRoles.TANKER_MEDIUM, 3f, 132 ShipRoles.TANKER_LARGE, 1f, 133 ShipRoles.PERSONNEL_SMALL, 10f, 134 ShipRoles.PERSONNEL_MEDIUM, 3f, 135 ShipRoles.PERSONNEL_LARGE, 1f 136 ); 137 return variantId; 138 } 139 public static String pickSmallVariantId(String factionId, Random random) { 140 String variantId = pickVariant(factionId, random, 141 ShipRoles.COMBAT_SMALL, 10f, 142 ShipRoles.COMBAT_FREIGHTER_SMALL, 3f, 143 ShipRoles.FREIGHTER_SMALL, 1f, 144 ShipRoles.TANKER_SMALL, 1f, 145 ShipRoles.LINER_SMALL, 1f, 146 ShipRoles.PERSONNEL_SMALL, 1f 147 ); 148 return variantId; 149 } 150 151 public static String pickMediumVariantId(String factionId, Random random) { 152 String variantId = pickVariant(factionId, random, 153 ShipRoles.COMBAT_MEDIUM, 10f, 154 ShipRoles.COMBAT_FREIGHTER_MEDIUM, 3f, 155 ShipRoles.CARRIER_SMALL, 1f, 156 ShipRoles.FREIGHTER_MEDIUM, 1f, 157 ShipRoles.TANKER_MEDIUM, 1f, 158 ShipRoles.LINER_MEDIUM, 1f, 159 ShipRoles.PERSONNEL_MEDIUM, 1f 160 ); 161 return variantId; 162 } 163 164 public static String pickLargeVariantId(String factionId, Random random) { 165 String variantId = pickVariant(factionId, random, 166 ShipRoles.COMBAT_LARGE, 10f, 167 ShipRoles.COMBAT_CAPITAL, 3f, 168 ShipRoles.COMBAT_FREIGHTER_LARGE, 1f, 169 ShipRoles.CARRIER_MEDIUM, 1f, 170 ShipRoles.FREIGHTER_LARGE, 1f, 171 ShipRoles.CARRIER_LARGE, 1f, 172 ShipRoles.TANKER_LARGE, 1f, 173 ShipRoles.LINER_LARGE, 1f, 174 ShipRoles.TANKER_MEDIUM, 1f, 175 ShipRoles.PERSONNEL_LARGE, 1f 176 ); 177 return variantId; 178 } 179 180 181 182 public static String pickVariant(String factionId, Random random, Object ... shipRoles) { 183 if (random == null) random = new Random(); 184 185 FactionAPI faction = Global.getSector().getFaction(factionId); 186 187 WeightedRandomPicker<String> picker = new WeightedRandomPicker<String>(random); 188 for (int i = 0; i < shipRoles.length; i += 2) { 189 String role = (String) shipRoles[i]; 190 Float weight = (Float) shipRoles[i + 1]; 191 picker.add(role, weight); 192 } 193 194 Set<String> variantsForRole = new HashSet<String>(); 195 while (variantsForRole.isEmpty() && !picker.isEmpty()) { 196 String role = picker.pickAndRemove(); 197 if (role == null) return null; 198 199 variantsForRole = faction.getVariantsForRole(role); 200 } 201 202 picker.clear(); 203 picker.addAll(variantsForRole); 204 String variantId = picker.pick(); 205 206 return variantId; 207 } 208 209 210 211 public static class DerelictShipData { 212 public PerShipData ship; 213 public float durationDays = 10000000f; 214 public boolean canHaveExtraCargo = false; 215 public DerelictShipData(PerShipData ship, boolean canHaveExtraCargo) { 216 this.ship = ship; 217 this.canHaveExtraCargo = canHaveExtraCargo; 218 } 219// public DerelictShipData(String variantId, ShipCondition condition, float duration, boolean canHaveExtraCargo) { 220// if (condition == null) condition = pickDerelictCondition(null); 221// if (duration <= 0) duration = 1000000000f; 222// durationDays = duration; 223// ship = new PerShipData(variantId, condition); 224// this.canHaveExtraCargo = canHaveExtraCargo; 225// } 226 } 227 228 //private CustomCampaignEntityAPI entity; 229 private DerelictShipData data; 230 231 private transient GenericCampaignEntitySprite sprite; 232 private transient FleetMemberAPI member; 233 private transient float scale; 234 235 private float angVel = 0f; 236 237 public void init(SectorEntityToken entity, Object params) { 238 super.init(entity, params); 239 //this.entity = (CustomCampaignEntityAPI) entity; 240 data = (DerelictShipData) params; 241 242 angVel = 5f + (float) Math.random() * 10f; 243 angVel *= Math.signum((float) Math.random() - 0.5f); 244 245 readResolve(); 246 247 entity.setSensorProfile(1f); 248 entity.setDiscoverable(false); 249 250 float range = getDetectedAtRange(member.getHullSpec().getHullSize()); 251 252 // "gen" is id used when spawning salvage entity by default 253 // so this overrides that value 254 entity.getDetectedRangeMod().modifyFlat("gen", range); 255 256 ((CustomCampaignEntityAPI)entity).setRadius(getRadius(member.getHullSpec().getHullSize())); 257 258 // add some default salvage 259 // some uses of this will want to clear that out and add something more specific 260 DropData data = new DropData(); 261 data.group = Drops.BASIC; 262 data.value = (int) getBasicDropValue(member); 263 entity.addDropValue(data); 264 265// data = new DropData(); 266// data.group = Drops.ANY_HULLMOD_LOW; 267// data.chances = 1; 268// entity.addDropRandom(data); 269 270 if (this.data.canHaveExtraCargo) { 271 // why add this as extraSavlage instead of drops? 272 // because needs to be based on cargo capacity not cargo value (which all drops are) 273 long seed = Misc.getSalvageSeed(entity); 274 Random r = Misc.getRandom(seed, 2); 275 float extraProb = 0.5f; 276 if (r.nextFloat() < extraProb) { 277 if (member.getVariant().isFreighter()) { 278 WeightedRandomPicker<DropGroupRow> picker = DropGroupRow.getPicker(Drops.FREIGHTER_CARGO); 279 picker.setRandom(new Random(seed)); 280 CargoAPI extraSalvage = Global.getFactory().createCargo(true); 281 for (int i = 0; i < 3; i++) { 282 DropGroupRow pick = picker.pick(); 283 if (pick.isCommodity()) { 284 extraSalvage.addCommodity(pick.getCommodity(), 285 (int)Math.ceil(member.getCargoCapacity() * (0.15f + 0.15f * r.nextFloat()))); 286 } 287 } 288 BaseSalvageSpecial.addExtraSalvage(extraSalvage, entity.getMemoryWithoutUpdate(), -1); 289 } else if (member.getVariant().isTanker()) { 290 CargoAPI extraSalvage = Global.getFactory().createCargo(true); 291 extraSalvage.addFuel((int)Math.ceil(member.getFuelCapacity() * (0.25f + 0.25f * r.nextFloat()))); 292 BaseSalvageSpecial.addExtraSalvage(extraSalvage, entity.getMemoryWithoutUpdate(), -1); 293 } 294 } 295 } 296 297 298 299 300 // can't be "discovered" by default, but something else could setDiscoverable(true) 301 // in which case this XP value will matter 302 entity.setDiscoveryXP((float) data.value * 0.05f); 303 304 entity.setSalvageXP((float) data.value * 0.15f); 305 306 //this.data.durationDays = 1f; 307 308 } 309 310 public static float getRadius(HullSize size) { 311 switch (size) { 312 case CAPITAL_SHIP: return 40f; 313 case CRUISER: return 35f; 314 case DESTROYER: return 30f; 315 case FRIGATE: return 25f; 316 } 317 return 20f; 318 } 319 320 public static float getBaseDuration(HullSize size) { 321 switch (size) { 322 case CAPITAL_SHIP: return 50f; 323 case CRUISER: return 40f; 324 case DESTROYER: return 30f; 325 case FRIGATE: return 25f; 326 } 327 return 25f; 328 } 329 330 public static float getDetectedAtRange(HullSize size) { 331 switch (size) { 332 case CAPITAL_SHIP: return 1700f; 333 case CRUISER: return 1300f; 334 case DESTROYER: return 1000f; 335 case FRIGATE: return 800f; 336 } 337 return 800f; 338 } 339 340 public static float getBasicDropValue(FleetMemberAPI member) { 341 float value = member.getDeploymentCostSupplies() * 200f; 342 return value; 343 } 344 345 Object readResolve() { 346 //sprite = new GenericFieldItemSprite(entity, category, key, cellSize, size, spawnRadius); 347 if (data.ship.variantId != null) { 348 member = Global.getFactory().createFleetMember(FleetMemberType.SHIP, data.ship.variantId); 349 } else { 350 member = Global.getFactory().createFleetMember(FleetMemberType.SHIP, data.ship.variant); 351 } 352 353 scale = Misc.getCampaignShipScaleMult(member.getHullSpec().getHullSize()); 354 355 //scale *= 5f; 356 357 sprite = new GenericCampaignEntitySprite(entity, member.getHullSpec().getSpriteName(), scale); 358 359 SpriteAPI base = Global.getSettings().getSprite(member.getHullSpec().getSpriteName()); 360 SpriteAPI overlay = Global.getSettings().getSprite("misc", "campaignDerelictOverlay"); 361 float w = base.getWidth(); 362 float h = base.getHeight(); 363 float size = Math.max(w, h) * 3f * scale; 364 overlay.setSize(size, size); 365 sprite.setOverlay(overlay); 366 367// SpriteAPI glow = Global.getSettings().getSprite("misc", "campaignDerelictOverlay"); 368// glow.setSize(size, size); 369// sprite.setGlow(glow); 370 371 return this; 372 } 373 374 protected float elapsed = 0f; 375 protected Boolean expiring = null; 376 public void advance(float amount) { 377 if (entity.isInCurrentLocation()) { 378 float turn = amount * angVel; 379 entity.setFacing(Misc.normalizeAngle(entity.getFacing() + turn)); 380 } 381 382// if (!entity.hasTag(Tags.NON_CLICKABLE)) { 383// Misc.fadeAndExpire(entity); 384// } 385 386 float days = Global.getSector().getClock().convertToDays(amount); 387 elapsed += days; 388 389 if (elapsed > data.durationDays && expiring == null) { 390 VisibilityLevel vis = entity.getVisibilityLevelToPlayerFleet(); 391 boolean playerCanSee = entity.isInCurrentLocation() && 392 (vis == VisibilityLevel.COMPOSITION_AND_FACTION_DETAILS || 393 vis == VisibilityLevel.COMPOSITION_DETAILS); 394 if (!playerCanSee) { 395 Misc.fadeAndExpire(entity, 1f); 396 expiring = true; 397 } 398 } 399 } 400 401 public float getRenderRange() { 402 return entity.getRadius() + 100f; 403 } 404 405 public void render(CampaignEngineLayers layer, ViewportAPI viewport) { 406 float alphaMult = viewport.getAlphaMult(); 407 alphaMult *= entity.getSensorFaderBrightness(); 408 alphaMult *= entity.getSensorContactFaderBrightness(); 409 if (alphaMult <= 0) return; 410 411 sprite.render(0, 0, entity.getFacing(), alphaMult); 412 } 413 414 public DerelictShipData getData() { 415 return data; 416 } 417 418 @Override 419 public void appendToCampaignTooltip(TooltipMakerAPI tooltip, VisibilityLevel level) { 420 // doesn't work, since those aren't rolled for until the ship is interacted-with 421// if (Misc.getCurrPermanentMods(data.ship.variant) > 0) { 422// float opad = 10f; 423// tooltip.addPara("Sensor returns are slightly abnormal.", opad); 424// } 425 } 426 427 428} 429 430 431 432 433 434 435 436 437 438