001package com.fs.starfarer.api.impl.campaign.ghosts; 002 003import java.util.ArrayList; 004import java.util.Iterator; 005import java.util.List; 006import java.util.Random; 007 008import com.fs.starfarer.api.EveryFrameScript; 009import com.fs.starfarer.api.Global; 010import com.fs.starfarer.api.campaign.CampaignFleetAPI; 011import com.fs.starfarer.api.campaign.SectorEntityToken; 012import com.fs.starfarer.api.impl.campaign.ghosts.types.AbyssalDrifterGhostCreator; 013import com.fs.starfarer.api.impl.campaign.ghosts.types.ChargerGhostCreator; 014import com.fs.starfarer.api.impl.campaign.ghosts.types.EchoGhostCreator; 015import com.fs.starfarer.api.impl.campaign.ghosts.types.EncounterTricksterGhostCreator; 016import com.fs.starfarer.api.impl.campaign.ghosts.types.GuideGhostCreator; 017import com.fs.starfarer.api.impl.campaign.ghosts.types.LeviathanCalfGhostCreator; 018import com.fs.starfarer.api.impl.campaign.ghosts.types.LeviathanGhostCreator; 019import com.fs.starfarer.api.impl.campaign.ghosts.types.MinnowGhostCreator; 020import com.fs.starfarer.api.impl.campaign.ghosts.types.NoGhostCreator; 021import com.fs.starfarer.api.impl.campaign.ghosts.types.RacerGhostCreator; 022import com.fs.starfarer.api.impl.campaign.ghosts.types.RemnantGhostCreator; 023import com.fs.starfarer.api.impl.campaign.ghosts.types.RemoraGhostCreator; 024import com.fs.starfarer.api.impl.campaign.ghosts.types.ShipGhostCreator; 025import com.fs.starfarer.api.impl.campaign.ghosts.types.StormTricksterGhostCreator; 026import com.fs.starfarer.api.impl.campaign.ghosts.types.StormcallerGhostCreator; 027import com.fs.starfarer.api.impl.campaign.ghosts.types.ZigguratGhostCreator; 028import com.fs.starfarer.api.impl.campaign.ids.MemFlags; 029import com.fs.starfarer.api.util.Misc; 030import com.fs.starfarer.api.util.TimeoutTracker; 031import com.fs.starfarer.api.util.WeightedRandomPicker; 032 033public class SensorGhostManager implements EveryFrameScript { 034 035 public static List<SensorGhostCreator> CREATORS = new ArrayList<SensorGhostCreator>(); 036 static { 037 //CREATORS.add(new TestGhostCreator()); 038 CREATORS.add(new ChargerGhostCreator()); 039 CREATORS.add(new EchoGhostCreator()); 040 CREATORS.add(new EncounterTricksterGhostCreator()); 041 CREATORS.add(new GuideGhostCreator()); 042 CREATORS.add(new LeviathanGhostCreator()); 043 CREATORS.add(new LeviathanCalfGhostCreator()); 044 CREATORS.add(new MinnowGhostCreator()); 045 CREATORS.add(new NoGhostCreator()); 046 CREATORS.add(new RacerGhostCreator()); 047 CREATORS.add(new RemnantGhostCreator()); 048 CREATORS.add(new RemoraGhostCreator()); 049 CREATORS.add(new ShipGhostCreator()); 050 CREATORS.add(new StormcallerGhostCreator()); 051 CREATORS.add(new StormTricksterGhostCreator()); 052 CREATORS.add(new ZigguratGhostCreator()); 053 CREATORS.add(new AbyssalDrifterGhostCreator()); 054 } 055 056 057 public static float GHOST_SPAWN_RATE_MULT = 0.75f; 058 059 public static float GHOST_SPAWN_RATE_MULT_IN_ABYSS = 3f; 060 061 public static float SB_ATTRACT_GHOSTS_PROBABILITY = 0.5f; 062 public static float SB_FAILED_TO_ATTRACT_TIMEOUT_MULT = 0.25f; 063 public static float MIN_SB_TIMEOUT = 5f; 064 public static float MAX_SB_TIMEOUT = 20f; 065 public static float MIN_FULL_GHOST_TIMEOUT_DAYS = 10f; 066 public static float MAX_FULL_GHOST_TIMEOUT_DAYS = 40f; 067 public static float MIN_SHORT_GHOST_TIMEOUT_DAYS = 0f; 068 public static float MAX_SHORT_GHOST_TIMEOUT_DAYS = 0.2f; 069 public static float FULL_TIMEOUT_TRIGGER_PROBABILITY = 0.95f; // chance spawning a ghost triggers the full timeout 070 071 072 public static float MIN_FAILED_CREATOR_TIMEOUT_DAYS = 0.8f; 073 public static float MAX_FAILED_CREATOR_TIMEOUT_DAYS = 1.2f; 074 075 076 protected TimeoutTracker<String> perCreatorTimeouts = new TimeoutTracker<String>(); 077 protected float timeoutRemaining = 0f; 078 protected float sbTimeoutRemaining = 0f; 079 protected Random random = new Random(Misc.genRandomSeed()); 080 protected List<SensorGhost> ghosts = new ArrayList<SensorGhost>(); 081 protected boolean spawnTriggeredBySensorBurst = false; 082 083 public static SensorGhostManager getGhostManager() { 084 String ghostManagerKey = "$ghostManager"; 085 SensorGhostManager manager = (SensorGhostManager) Global.getSector().getMemoryWithoutUpdate().get(ghostManagerKey); 086 if (manager == null) { 087 for (EveryFrameScript curr : Global.getSector().getScripts()) { 088 if (curr instanceof SensorGhostManager) { 089 manager = (SensorGhostManager) curr; 090 Global.getSector().getMemoryWithoutUpdate().set(ghostManagerKey, manager); 091 break; 092 } 093 } 094 } 095 return manager; 096 } 097 098 public static SensorGhost getGhostFor(SectorEntityToken entity) { 099 SensorGhostManager manager = getGhostManager(); 100 if (manager == null) return null; 101 102 for (SensorGhost ghost : manager.ghosts) { 103 if (ghost.getEntity() == entity) { 104 return ghost; 105 } 106 } 107 return null; 108 } 109 110 public void advance(float amount) { 111 if (amount == 0) return; 112 CampaignFleetAPI pf = Global.getSector().getPlayerFleet(); 113 if (pf == null) return; 114 115 float days = Global.getSector().getClock().convertToDays(amount); 116 117 if (Misc.getAbyssalDepth(pf) >= 1f) { 118 days *= GHOST_SPAWN_RATE_MULT_IN_ABYSS; 119 } 120 121 perCreatorTimeouts.advance(days); 122 123 124 sbTimeoutRemaining -= days; 125 if (sbTimeoutRemaining <= 0f) { 126 sbTimeoutRemaining = 0f; 127 checkSensorBursts(); 128 } 129 130 timeoutRemaining -= days * GHOST_SPAWN_RATE_MULT; 131 if (timeoutRemaining <= 0f) { 132 spawnGhost(); 133 spawnTriggeredBySensorBurst = false; 134 } 135 136 Iterator<SensorGhost> iter = ghosts.iterator(); 137 while (iter.hasNext()) { 138 SensorGhost curr = iter.next(); 139 curr.advance(amount); 140 if (curr.isDone()) { 141 iter.remove(); 142 } 143 } 144 } 145 146 public boolean isSpawnTriggeredBySensorBurst() { 147 return spawnTriggeredBySensorBurst; 148 } 149 150 public void checkSensorBursts() { 151 if (!Global.getSector().getCurrentLocation().isHyperspace()) return; 152 if (timeoutRemaining < 1f) return; 153 if (Global.getSector().getMemoryWithoutUpdate().getBoolean(MemFlags.GLOBAL_SENSOR_BURST_JUST_USED_IN_CURRENT_LOCATION)) { 154 if (random.nextFloat() > SB_ATTRACT_GHOSTS_PROBABILITY) { 155 sbTimeoutRemaining = MIN_SB_TIMEOUT + (MAX_SB_TIMEOUT - MIN_SB_TIMEOUT) * random.nextFloat(); 156 sbTimeoutRemaining *= SB_FAILED_TO_ATTRACT_TIMEOUT_MULT; 157 return; 158 } 159 CampaignFleetAPI pf = Global.getSector().getPlayerFleet(); 160 float range = 2000f; 161 for (CampaignFleetAPI fleet : Global.getSector().getCurrentLocation().getFleets()) { 162 float dist = Misc.getDistance(fleet.getLocation(), pf.getLocation()); 163 if (dist > range) continue; 164 if (fleet.getMemoryWithoutUpdate().getBoolean(MemFlags.JUST_DID_SENSOR_BURST)) { 165 timeoutRemaining = 0.2f + 0.8f * random.nextFloat(); 166 spawnTriggeredBySensorBurst = true; 167 sbTimeoutRemaining = MIN_SB_TIMEOUT + (MAX_SB_TIMEOUT - MIN_SB_TIMEOUT) * random.nextFloat(); 168 break; 169 } 170 } 171 } 172 } 173 174 public void spawnGhost() { 175 CampaignFleetAPI pf = Global.getSector().getPlayerFleet(); 176 boolean nearStream = Misc.isInsideSlipstream(pf.getLocation(), 1000f, pf.getContainingLocation()); 177 178 boolean inAbyss = Misc.isInAbyss(pf); 179 180 WeightedRandomPicker<SensorGhostCreator> picker = new WeightedRandomPicker<SensorGhostCreator>(random); 181 for (SensorGhostCreator creator : CREATORS) { 182 if (perCreatorTimeouts.contains(creator.getId())) continue; 183 if (nearStream && !creator.canSpawnWhilePlayerInOrNearSlipstream()) continue; 184 if (inAbyss && !creator.canSpawnWhilePlayerInAbyss()) continue; 185 if (!inAbyss && !creator.canSpawnWhilePlayerOutsideAbyss()) continue; 186 187 float freq = creator.getFrequency(this); 188 picker.add(creator, freq); 189 } 190 191 SensorGhostCreator creator = picker.pick(); 192 if (creator == null) return; 193 194 //System.out.println("Picked: " + creator.getId()); 195 196 boolean canSpawn = true; 197 // important: the creator that can't spawn a ghost can still be picked, just won't fire 198 // otherwise moving in/out of slipstreams would manipulate ghost spawning 199 // can still manipulate it since it causes a "failed to create" timeout rather than a "created" one, 200 // but that should be a bit less noticeable 201// if (!creator.canSpawnWhilePlayerInOrNearSlipstream()) { 202// //CampaignFleetAPI pf = Global.getSector().getPlayerFleet(); 203// canSpawn = !Misc.isInsideSlipstream(pf.getLocation(), 1000f, pf.getContainingLocation()); 204// } 205 206 List<SensorGhost> ghosts = null; 207 if (canSpawn) { 208 ghosts = creator.createGhost(this); 209 } 210 boolean anyFailed = false; // bit of a failsafe if a creator returns a failed-to-spawn ghost 211 if (ghosts != null) { 212 for (SensorGhost curr : ghosts) { 213 anyFailed |= curr.isCreationFailed(); 214 curr.setDespawnInAbyss(!creator.canSpawnWhilePlayerInAbyss()); 215 } 216 } 217 if (!canSpawn) { 218 anyFailed = true; 219 } 220 221 if (ghosts == null || ghosts.isEmpty() || anyFailed) { 222 float timeout = MIN_FAILED_CREATOR_TIMEOUT_DAYS + 223 random.nextFloat() * (MAX_FAILED_CREATOR_TIMEOUT_DAYS - MIN_FAILED_CREATOR_TIMEOUT_DAYS); 224 perCreatorTimeouts.set(creator.getId(), timeout); 225 } else { 226 this.ghosts.addAll(ghosts); 227 if (random.nextFloat() < FULL_TIMEOUT_TRIGGER_PROBABILITY) { 228 timeoutRemaining = MIN_FULL_GHOST_TIMEOUT_DAYS + 229 random.nextFloat() * (MAX_FULL_GHOST_TIMEOUT_DAYS - MIN_FULL_GHOST_TIMEOUT_DAYS); 230 } else { 231 timeoutRemaining = MIN_SHORT_GHOST_TIMEOUT_DAYS + 232 random.nextFloat() * (MAX_SHORT_GHOST_TIMEOUT_DAYS - MIN_SHORT_GHOST_TIMEOUT_DAYS); 233 } 234 perCreatorTimeouts.set(creator.getId(), creator.getTimeoutDaysOnSuccessfulCreate(this)); 235 } 236 } 237 238 239 public boolean hasGhostOfClass(Class<?> clazz) { 240 for (SensorGhost ghost : ghosts) { 241 if (clazz.isInstance(ghost)) return true; 242 } 243 return false; 244 } 245 246 public Random getRandom() { 247 return random; 248 } 249 250 public boolean runWhilePaused() { 251 return false; 252 } 253 254 public boolean isDone() { 255 return false; 256 } 257 258 public List<SensorGhost> getGhosts() { 259 return ghosts; 260 } 261 262 263}