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