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}