001package com.fs.starfarer.api.impl.campaign.intel.bases; 002 003import java.util.ArrayList; 004import java.util.Collections; 005import java.util.Comparator; 006import java.util.LinkedHashMap; 007import java.util.LinkedHashSet; 008import java.util.List; 009import java.util.Random; 010 011import com.fs.starfarer.api.EveryFrameScript; 012import com.fs.starfarer.api.Global; 013import com.fs.starfarer.api.campaign.StarSystemAPI; 014import com.fs.starfarer.api.campaign.econ.Industry; 015import com.fs.starfarer.api.campaign.econ.MarketAPI; 016import com.fs.starfarer.api.impl.campaign.DebugFlags; 017import com.fs.starfarer.api.impl.campaign.Tuning; 018import com.fs.starfarer.api.impl.campaign.ids.Factions; 019import com.fs.starfarer.api.impl.campaign.ids.Tags; 020import com.fs.starfarer.api.impl.campaign.intel.BaseEventManager; 021import com.fs.starfarer.api.impl.campaign.intel.events.LuddicPathHostileActivityFactor; 022import com.fs.starfarer.api.util.IntervalUtil; 023import com.fs.starfarer.api.util.Misc; 024import com.fs.starfarer.api.util.Pair; 025import com.fs.starfarer.api.util.WeightedRandomPicker; 026 027public class LuddicPathBaseManager extends BaseEventManager { 028 029 public static float LUDDIC_CHURCH_INTEREST_MULT = 0.1f; 030 public static float PLAYER_DEFEATED_PATHER_CRISIS_INTEREST_MULT = 0.5f; 031 032 public static float AI_CORE_ADMIN_INTEREST = 10f; 033 034 public static final String KEY = "$core_luddicPathBaseManager"; 035 036 public static final float INERTIA_DAYS_MAX = 30f; 037 public static final float INERTIA_DAYS_MIN = 10f; 038 039 public static final float CHECK_DAYS = 10f; 040 public static final float CHECK_PROB = 0.5f; 041 042 043 public static LuddicPathBaseManager getInstance() { 044 Object test = Global.getSector().getMemoryWithoutUpdate().get(KEY); 045 return (LuddicPathBaseManager) test; 046 } 047 048 protected long start = 0; 049 050 public LuddicPathBaseManager() { 051 super(); 052 Global.getSector().getMemoryWithoutUpdate().set(KEY, this); 053 start = Global.getSector().getClock().getTimestamp(); 054 } 055 056 @Override 057 protected int getMinConcurrent() { 058 return Global.getSettings().getInt("minLPBases"); 059 } 060 @Override 061 protected int getMaxConcurrent() { 062 return Global.getSettings().getInt("maxLPBases"); 063 } 064 065 @Override 066 protected float getBaseInterval() { 067 return CHECK_DAYS; 068 } 069 070 @Override 071 protected Object readResolve() { 072 super.readResolve(); 073 if (cellChecker == null) { 074 cellChecker = new IntervalUtil(1f, 3f); 075 } 076 if (cells == null) { 077 cells = new LinkedHashMap<MarketAPI, LuddicPathCellsIntel>(); 078 } 079 return this; 080 } 081 082 protected IntervalUtil cellChecker = new IntervalUtil(1f, 3f); 083 protected int timesSinceLastChange = 10000; 084 protected int activeMod = 0; 085 protected int sleeperMod = 0; 086 087 @Override 088 public void advance(float amount) { 089 super.advance(amount); 090 091 float days = Misc.getDays(amount); 092 cellChecker.advance(days); 093 if (cellChecker.intervalElapsed()) { 094 timesSinceLastChange++; 095 if (timesSinceLastChange > 50) { 096 activeMod = Misc.random.nextInt(3); 097 sleeperMod = Misc.random.nextInt(3); 098 timesSinceLastChange = 0; 099 } 100 101 updateCellStatus(); 102 } 103 } 104 105 protected LinkedHashMap<MarketAPI, LuddicPathCellsIntel> cells = new LinkedHashMap<MarketAPI, LuddicPathCellsIntel>(); 106 107 protected void updateCellStatus() { 108 109 float fraction = Global.getSettings().getFloat("basePatherCellFraction"); 110 float minInterest = Global.getSettings().getFloat("minInterestForPatherCells"); 111 112 List<Pair<MarketAPI, Float>> marketAndScore = new ArrayList<Pair<MarketAPI,Float>>(); 113 int total = 0; 114 for (MarketAPI market : Global.getSector().getEconomy().getMarketsCopy()) { 115 if (market.getEconGroup() != null) continue; 116 if (market.getSize() < 4) continue; 117 float score = getLuddicPathMarketInterest(market); 118 total++; 119 if (score >= minInterest) { 120 marketAndScore.add(new Pair<MarketAPI, Float>(market, score)); 121 } 122 } 123 124 int numActive = Math.round(total * fraction); 125 int numSleeper = numActive; 126 127 numActive += activeMod; 128 numSleeper += sleeperMod; 129 130 LinkedHashSet<MarketAPI> active = new LinkedHashSet<MarketAPI>(); 131 LinkedHashSet<MarketAPI> sleeper = new LinkedHashSet<MarketAPI>(); 132 133 134 Collections.sort(marketAndScore, new Comparator<Pair<MarketAPI, Float>>() { 135 public int compare(Pair<MarketAPI, Float> o1, Pair<MarketAPI, Float> o2) { 136 return (int) Math.signum(o2.two - o1.two); 137 } 138 }); 139 140 int count = 0; 141 for (Pair<MarketAPI, Float> p : marketAndScore) { 142 if (count < numActive) { 143 active.add(p.one); 144 } else if (count < numActive + numSleeper) { 145 sleeper.add(p.one); 146 } else { 147 break; 148 } 149 count++; 150 } 151 152 153 for (MarketAPI market : new ArrayList<MarketAPI>(cells.keySet())) { 154 LuddicPathCellsIntel intel = cells.get(market); 155 156 if (!active.contains(market) && !sleeper.contains(market)) { 157 158 float score = getLuddicPathMarketInterest(market); 159 160 if (intel.getInertiaTime() >= INERTIA_DAYS_MAX || 161 (intel.getInertiaTime() >= INERTIA_DAYS_MIN && score < minInterest)) { 162 if (!intel.isEnding()) { 163 intel.endAfterDelay(); 164 if (market.isPlayerOwned() || DebugFlags.PATHER_BASE_DEBUG) { 165 intel.sendUpdateIfPlayerHasIntel(LuddicPathCellsIntel.UPDATE_DISSOLVED, false); 166 } 167 } 168 cells.remove(market); 169 } else { // keep already-established cells for up to INERTIA_DAYS_MAX, at the expense of other potential cells 170 if (intel.isSleeper()) { 171 List<MarketAPI> sleeperList = new ArrayList<MarketAPI>(sleeper); 172 for (int i = sleeperList.size() - 1; i >= 0; i--) { 173 MarketAPI other = sleeperList.get(i); 174 LuddicPathCellsIntel otherIntel = cells.get(other); 175 if (otherIntel != null) continue; 176 177 sleeper.remove(other); 178 break; 179 } 180 sleeper.add(market); 181 } else { 182 List<MarketAPI> activeList = new ArrayList<MarketAPI>(active); 183 for (int i = activeList.size() - 1; i >= 0; i--) { 184 MarketAPI other = activeList.get(i); 185 LuddicPathCellsIntel otherIntel = cells.get(other); 186 if (otherIntel != null) continue; 187 188 active.remove(other); 189 break; 190 } 191 active.add(market); 192 } 193 } 194 } else { 195 intel.setInertiaTime(0f); 196 } 197 } 198 199 for (MarketAPI market : active) { 200 LuddicPathCellsIntel intel = cells.get(market); 201 LuddicPathBaseIntel base = LuddicPathCellsIntel.getClosestBase(market); 202 if (intel == null) { 203 intel = new LuddicPathCellsIntel(market, base == null); 204 cells.put(market, intel); 205 } 206 if (base != null) { 207 intel.makeActiveIfPossible(); 208 } else { 209 intel.makeSleeper(); 210 } 211 } 212 213 for (MarketAPI market : sleeper) { 214 LuddicPathCellsIntel intel = cells.get(market); 215 if (intel == null) { 216 intel = new LuddicPathCellsIntel(market, true); 217 cells.put(market, intel); 218 } 219 intel.makeSleeper(); 220 } 221 } 222 223 public static float getLuddicPathMarketInterest(MarketAPI market) { 224 if (market.getFactionId().equals(Factions.LUDDIC_PATH)) return 0f; 225 float total = 0f; 226 227 String aiCoreId = market.getAdmin().getAICoreId(); 228 if (aiCoreId != null) { 229 total += AI_CORE_ADMIN_INTEREST; 230 } 231 232 for (Industry ind : market.getIndustries()) { 233 total += ind.getPatherInterest(); 234 } 235 236 if (total > 0) { 237 total += new Random(market.getName().hashCode()).nextFloat() * 0.1f; 238 } 239 240 if (market.getFactionId().equals(Factions.LUDDIC_CHURCH)) { 241 total *= LUDDIC_CHURCH_INTEREST_MULT; 242 } 243 244 if (market.isPlayerOwned() && LuddicPathHostileActivityFactor.isPlayerDefeatedPatherExpedition()) { 245 total *= PLAYER_DEFEATED_PATHER_CRISIS_INTEREST_MULT; 246 } 247 248 return total; 249 } 250 251 252 253 protected Random random = new Random(); 254 @Override 255 protected EveryFrameScript createEvent() { 256 if (numSpawnChecksToSkip > 0) { 257 numSpawnChecksToSkip--; 258 return null; 259 } 260 261 if (random.nextFloat() < CHECK_PROB) return null; 262 263 StarSystemAPI system = pickSystemForLPBase(); 264 if (system == null) return null; 265 266 String factionId = Factions.LUDDIC_PATH; 267 268 LuddicPathBaseIntel intel = new LuddicPathBaseIntel(system, factionId); 269 if (intel.isDone()) intel = null; 270 271 return intel; 272 } 273 274 275 protected StarSystemAPI pickSystemForLPBase() { 276 WeightedRandomPicker<StarSystemAPI> far = new WeightedRandomPicker<StarSystemAPI>(random); 277 WeightedRandomPicker<StarSystemAPI> picker = new WeightedRandomPicker<StarSystemAPI>(random); 278 279 for (StarSystemAPI system : Global.getSector().getStarSystems()) { 280 if (system.hasTag(Tags.THEME_SPECIAL)) continue; 281 if (system.hasTag(Tags.THEME_HIDDEN)) continue; 282 283 float days = Global.getSector().getClock().getElapsedDaysSince(system.getLastPlayerVisitTimestamp()); 284 if (days < 45f) continue; 285 286 if (system.getCenter().getMemoryWithoutUpdate().contains(PirateBaseManager.RECENTLY_USED_FOR_BASE)) continue; 287 288 float weight = 0f; 289 if (system.hasTag(Tags.THEME_MISC_SKIP)) { 290 weight = 1f; 291 } else if (system.hasTag(Tags.THEME_MISC)) { 292 weight = 3f; 293 } else if (system.hasTag(Tags.THEME_REMNANT_NO_FLEETS)) { 294 weight = 3f; 295 } else if (system.hasTag(Tags.THEME_RUINS)) { 296 weight = 5f; 297 } else if (system.hasTag(Tags.THEME_CORE_UNPOPULATED)) { 298 weight = 1f; 299 } 300 if (weight <= 0f) continue; 301 302 float usefulStuff = system.getCustomEntitiesWithTag(Tags.OBJECTIVE).size() + 303 system.getCustomEntitiesWithTag(Tags.STABLE_LOCATION).size(); 304 if (usefulStuff <= 0) continue; 305 306 if (Misc.hasPulsar(system)) continue; 307 if (Misc.getMarketsInLocation(system).size() > 0) continue; 308 309 float dist = system.getLocation().length(); 310 311 312// float distMult = 1f - dist / 20000f; 313// if (distMult > 1f) distMult = 1f; 314// if (distMult < 0.1f) distMult = 0.1f; 315 316 float distMult = 1f; 317 318 if (dist > 36000f) { 319 far.add(system, weight * usefulStuff * distMult); 320 } else { 321 picker.add(system, weight * usefulStuff * distMult); 322 } 323 } 324 325 if (picker.isEmpty()) { 326 picker.addAll(far); 327 } 328 329 return picker.pick(); 330 } 331 332 333 protected int numDestroyed = 0; 334 protected int numSpawnChecksToSkip = 0; 335 336 public void incrDestroyed() { 337 numDestroyed++; 338 numSpawnChecksToSkip = Math.max(numSpawnChecksToSkip, (Tuning.PATHER_BASE_MIN_TIMEOUT_MONTHS + 339 Misc.random.nextInt(Tuning.PATHER_BASE_MAX_TIMEOUT_MONTHS - Tuning.PATHER_BASE_MIN_TIMEOUT_MONTHS + 1)) 340 * 3); // checks happen every 10 days on average, *3 to get months 341 } 342} 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357