001package com.fs.starfarer.api.impl.campaign.abilities;
002
003import java.awt.Color;
004
005import com.fs.starfarer.api.EveryFrameScript;
006import com.fs.starfarer.api.Global;
007import com.fs.starfarer.api.campaign.BattleAPI;
008import com.fs.starfarer.api.campaign.CampaignFleetAPI;
009import com.fs.starfarer.api.campaign.SectorEntityToken.VisibilityLevel;
010import com.fs.starfarer.api.campaign.ai.FleetAIFlags;
011import com.fs.starfarer.api.campaign.ai.ModularFleetAIAPI;
012import com.fs.starfarer.api.campaign.econ.MarketAPI;
013import com.fs.starfarer.api.campaign.rules.MemoryAPI;
014import com.fs.starfarer.api.characters.AbilityPlugin;
015import com.fs.starfarer.api.impl.campaign.CoreReputationPlugin.RepActionEnvelope;
016import com.fs.starfarer.api.impl.campaign.CoreReputationPlugin.RepActions;
017import com.fs.starfarer.api.impl.campaign.ids.Abilities;
018import com.fs.starfarer.api.impl.campaign.ids.MemFlags;
019import com.fs.starfarer.api.impl.campaign.ids.Pings;
020import com.fs.starfarer.api.loading.CampaignPingSpec;
021import com.fs.starfarer.api.ui.LabelAPI;
022import com.fs.starfarer.api.ui.TooltipMakerAPI;
023import com.fs.starfarer.api.util.Misc;
024
025public class InterdictionPulseAbility extends BaseDurationAbility {
026
027        public static class IPReactionScript implements EveryFrameScript {
028                float delay;
029                boolean done;
030                CampaignFleetAPI other;
031                CampaignFleetAPI fleet;
032                float activationDays;
033                /**
034                 * fleet is using IP, other is reacting.
035                 * @param fleet
036                 * @param other
037                 * @param activationDays
038                 */
039                public IPReactionScript(CampaignFleetAPI fleet, CampaignFleetAPI other, float activationDays) {
040                        this.fleet = fleet;
041                        this.other = other;
042                        this.activationDays = activationDays;
043                        delay = 0.3f + 0.3f * (float) Math.random();
044                        //delay = 0f;
045                }
046                public void advance(float amount) {
047                        if (done) return;
048                        
049                        delay -= amount;
050                        if (delay > 0) return;
051                        
052                        VisibilityLevel level = fleet.getVisibilityLevelTo(other);
053                        if (level == VisibilityLevel.NONE || level == VisibilityLevel.SENSOR_CONTACT) {
054                                done = true;
055                                return;
056                        }
057                        
058                        if (!(other.getAI() instanceof ModularFleetAIAPI)) {
059                                done = true;
060                                return;
061                        }
062                        ModularFleetAIAPI ai = (ModularFleetAIAPI) other.getAI();
063                        
064                        
065                        float dist = Misc.getDistance(fleet.getLocation(), other.getLocation());
066                        float speed = Math.max(1f, other.getTravelSpeed());
067                        float eta = dist / speed;
068                        
069                        float rushTime = activationDays * Global.getSector().getClock().getSecondsPerDay();
070                        rushTime += 0.5f + 0.5f * (float) Math.random();
071                        
072                        MemoryAPI mem = other.getMemoryWithoutUpdate();
073                        CampaignFleetAPI pursueTarget = mem.getFleet(FleetAIFlags.PURSUIT_TARGET);
074                        
075                        if (eta < rushTime && pursueTarget == fleet) {
076                                done = true;
077                                return;
078                        }
079                        
080                        float range = InterdictionPulseAbility.getRange(fleet);
081                        float getAwayTime = 1f + (range - dist) / speed;
082                        AbilityPlugin sb = other.getAbility(Abilities.SENSOR_BURST);
083                        if (getAwayTime > rushTime && sb != null && sb.isUsable() && (float) Math.random() > 0.67f) {
084                                sb.activate();
085                                done = true;
086                                return;
087                        }
088                        
089                        //float avoidRange = Math.min(dist, getRange(other));
090                        float avoidRange = getRange(other) + 100f;
091                        ai.getNavModule().avoidLocation(fleet.getContainingLocation(), 
092                                                                                        fleet.getLocation(), avoidRange, avoidRange + 50f, activationDays + 0.01f);
093                        
094                        ai.getNavModule().avoidLocation(fleet.getContainingLocation(), 
095                                                                                        //fleet.getLocation(), dist, dist + 50f, activationDays + 0.01f);
096                                        Misc.getPointAtRadius(fleet.getLocation(), avoidRange * 0.5f), avoidRange, avoidRange * 1.5f + 50f, activationDays + 0.05f);
097                        
098                        done = true;
099                }
100
101                public boolean isDone() {
102                        return done;
103                }
104                public boolean runWhilePaused() {
105                        return false;
106                }
107        }
108        
109        public static final float MAX_EFFECT = 1f;
110        //public static final float RANGE = 1000f;
111        public static final float BASE_RANGE = 500f;
112        public static final float BASE_SECONDS = 6f;
113        public static final float STRENGTH_PER_SECOND = 200f;
114        
115        //public static final float CR_COST_MULT = 0.5f;
116        public static final float DETECTABILITY_PERCENT = 100f;
117        
118//      public String getSpriteName() {
119//              return Global.getSettings().getSpriteName("abilities", Abilities.EMERGENCY_BURN);
120//      }
121        
122
123        public static float getRange(CampaignFleetAPI fleet) {
124                float max = Global.getSettings().getMaxSensorRange();
125                return Math.min(max, BASE_RANGE + fleet.getSensorRangeMod().computeEffective(fleet.getSensorStrength()) / 2f);
126        }
127        
128        @Override
129        protected String getActivationText() {
130                //return Misc.ucFirst(spec.getName().toLowerCase());
131                return "Interdiction pulse";
132        }
133
134
135        protected Boolean primed = null;
136        protected Float elapsed = null;
137        protected Integer numFired = null;
138        
139        @Override
140        protected void activateImpl() {
141                CampaignFleetAPI fleet = getFleet();
142                if (fleet == null) return;
143                
144                Global.getSector().addPing(fleet, Pings.INTERDICT);
145                
146                float range = getRange(fleet);
147                for (CampaignFleetAPI other : fleet.getContainingLocation().getFleets()) {
148                        if (other == fleet) continue;
149                        
150                        float dist = Misc.getDistance(fleet.getLocation(), other.getLocation());
151                        if (dist > range + 500f) continue;
152
153                        other.addScript(new IPReactionScript(fleet, other, getActivationDays()));
154                }
155                
156                primed = true;
157                
158        }
159        
160        protected void showRangePing(float amount) {
161                CampaignFleetAPI fleet = getFleet();
162                if (fleet == null) return;
163                
164                VisibilityLevel vis = fleet.getVisibilityLevelToPlayerFleet();
165                if (vis == VisibilityLevel.NONE || vis == VisibilityLevel.SENSOR_CONTACT) return;
166                
167                
168                boolean fire = false;
169                if (elapsed == null) {
170                        elapsed = 0f;
171                        numFired = 0;
172                        fire = true;
173                }
174                elapsed += amount;
175                if (elapsed > 0.5f && numFired < 4) {
176                        elapsed -= 0.5f;
177                        fire = true;
178                }
179                
180                if (fire) {
181                        numFired++;
182                        
183                        float range = getRange(fleet);
184                        CampaignPingSpec custom = new CampaignPingSpec();
185                        custom.setUseFactionColor(true);
186                        custom.setWidth(7);
187                        custom.setMinRange(range - 100f);
188                        custom.setRange(200);
189                        custom.setDuration(2f);
190                        custom.setAlphaMult(0.25f);
191                        custom.setInFraction(0.2f);
192                        custom.setNum(1);
193                        
194                        Global.getSector().addPing(fleet, custom);
195                }
196                
197        }
198
199        @Override
200        protected void applyEffect(float amount, float level) {
201                CampaignFleetAPI fleet = getFleet();
202                if (fleet == null) return;
203                
204                fleet.getStats().getDetectedRangeMod().modifyPercent(getModId(), DETECTABILITY_PERCENT * level, "Interdiction pulse");
205                
206                //System.out.println("Level: " + level);
207                
208                if (level > 0 && level < 1 && amount > 0) {
209                        showRangePing(amount);
210//                      float activateSeconds = getActivationDays() * Global.getSector().getClock().getSecondsPerDay();
211//                      float speed = fleet.getVelocity().length();
212//                      float acc = Math.max(speed, 200f)/activateSeconds + fleet.getAcceleration();
213//                      float ds = acc * amount;
214//                      if (ds > speed) ds = speed;
215//                      Vector2f dv = Misc.getUnitVectorAtDegreeAngle(Misc.getAngleInDegrees(fleet.getVelocity()));
216//                      dv.scale(ds);
217//                      fleet.setVelocity(fleet.getVelocity().x - dv.x, fleet.getVelocity().y - dv.y);
218                        fleet.goSlowOneFrame();
219                        return;
220                }
221                
222                float range = getRange(fleet);
223                
224                boolean playedHit = !(entity.isInCurrentLocation() && entity.isVisibleToPlayerFleet());
225                if (level == 1 && primed != null) {
226                        
227                        if (entity.isInCurrentLocation()) {
228                                Global.getSector().getMemoryWithoutUpdate().set(MemFlags.GLOBAL_INTERDICTION_PULSE_JUST_USED_IN_CURRENT_LOCATION, true, 0.1f);
229                        }
230                        fleet.getMemoryWithoutUpdate().set(MemFlags.JUST_DID_INTERDICTION_PULSE, true, 0.1f);
231                        
232                        CampaignPingSpec custom = new CampaignPingSpec();
233                        custom.setUseFactionColor(true);
234                        custom.setWidth(15);
235                        custom.setRange(range * 1.3f);
236                        custom.setDuration(0.5f);
237                        custom.setAlphaMult(1f);
238                        custom.setInFraction(0.1f);
239                        custom.setNum(1);
240                        Global.getSector().addPing(fleet, custom);
241
242                        
243                        for (CampaignFleetAPI other : fleet.getContainingLocation().getFleets()) {
244                                if (other == fleet) continue;
245                                if (other.getFaction() == fleet.getFaction()) continue;
246                                if (other.isInHyperspaceTransition()) continue;
247                                
248                                float dist = Misc.getDistance(fleet.getLocation(), other.getLocation());
249                                if (dist > range) continue;
250                                
251                                
252                                float interdictSeconds = getInterdictSeconds(fleet, other);
253                                if (interdictSeconds > 0 && interdictSeconds < 1f) interdictSeconds = 1f;
254                                
255                                VisibilityLevel vis = other.getVisibilityLevelToPlayerFleet();
256                                if (vis == VisibilityLevel.COMPOSITION_AND_FACTION_DETAILS ||
257                                                vis == VisibilityLevel.COMPOSITION_DETAILS ||
258                                                (vis == VisibilityLevel.SENSOR_CONTACT && fleet.isPlayerFleet())) {
259                                        if (interdictSeconds <= 0) {
260                                                other.addFloatingText("Interdict avoided!" , fleet.getFaction().getBaseUIColor(), 1f, true);
261                                                continue;
262                                        } else {
263                                                other.addFloatingText("Interdict! (" + (int) Math.round(interdictSeconds) + "s)" , fleet.getFaction().getBaseUIColor(), 1f, true);
264                                        }
265                                }
266                                
267                                float interdictDays = interdictSeconds / Global.getSector().getClock().getSecondsPerDay();
268                                
269                                for (AbilityPlugin ability : other.getAbilities().values()) {
270                                        if (!ability.getSpec().hasTag(Abilities.TAG_BURN + "+") &&
271                                                        !ability.getSpec().hasTag(Abilities.TAG_DISABLED_BY_INTERDICT) &&
272                                                        !ability.getId().equals(Abilities.INTERDICTION_PULSE)) continue;
273                                        
274                                        float origCooldown = ability.getCooldownLeft();
275                                        float extra = 0;
276                                        if (ability.isActiveOrInProgress()) {
277                                                extra += ability.getSpec().getDeactivationCooldown() * ability.getProgressFraction();
278                                                ability.deactivate();
279                                                
280                                        }
281                                        
282                                        if (!ability.getSpec().hasTag(Abilities.TAG_BURN + "+")) continue;
283                                        
284                                        float cooldown = interdictDays;
285                                        //cooldown = Math.max(cooldown, origCooldown);
286                                        cooldown += origCooldown;
287                                        cooldown += extra;
288                                        float max = Math.max(ability.getSpec().getDeactivationCooldown(), 2f);
289                                        if (cooldown > max) cooldown = max;
290                                        ability.setCooldownLeft(cooldown);
291                                }
292                                
293                                if (fleet.isPlayerFleet() && other.knowsWhoPlayerIs() && fleet.getFaction() != other.getFaction()) {
294                                        Global.getSector().adjustPlayerReputation(
295                                                                                new RepActionEnvelope(RepActions.INTERDICTED, null, null, false), 
296                                                                                other.getFaction().getId());
297                                }
298                                
299                                if (!playedHit) {
300                                        Global.getSoundPlayer().playSound("world_interdict_hit", 1f, 1f, other.getLocation(), other.getVelocity());
301                                        //playedHit = true;
302                                }
303                        }
304                        
305                        primed = null;
306                        elapsed = null;
307                        numFired = null;
308                }
309                
310        }
311        
312        public static float getInterdictSeconds(CampaignFleetAPI fleet, CampaignFleetAPI other) {
313                float offense = fleet.getSensorRangeMod().computeEffective(fleet.getSensorStrength());
314                float defense = other.getSensorRangeMod().computeEffective(other.getSensorStrength());
315                float diff = offense - defense;
316                
317                float extra = diff / STRENGTH_PER_SECOND;
318                
319                float total = BASE_SECONDS + extra;
320                if (total < 0f) total = 0f;
321                return total;// / Global.getSector().getClock().getSecondsPerDay();
322        }
323        
324        
325//      public static float getEffectMagnitude(CampaignFleetAPI fleet, CampaignFleetAPI other) {
326//              float burn = Misc.getBurnLevelForSpeed(other.getVelocity().length());
327//              
328//              Vector2f velDir = Misc.normalise(new Vector2f(other.getVelocity()));
329//              Vector2f toFleet = Misc.normalise(Vector2f.sub(fleet.getLocation(), other.getLocation(), new Vector2f()));
330//              float dot = Vector2f.dot(velDir, toFleet);
331//              if (dot <= 0.05f || burn <= 1f) return 0f;
332//              
333//              float effect = dot;
334//              if (effect < 0) effect = 0;
335//              if (effect > 1) effect = 1;
336//              
337//              //effect *= Math.min(1f, burn / 10f);
338//              
339//              //return effect;
340//              return Math.max(0.1f, effect);
341//      }
342
343        @Override
344        protected void deactivateImpl() {
345                cleanupImpl();
346        }
347        
348        @Override
349        protected void cleanupImpl() {
350                CampaignFleetAPI fleet = getFleet();
351                if (fleet == null) return;
352                
353                fleet.getStats().getDetectedRangeMod().unmodify(getModId());
354                //fleet.getStats().getSensorRangeMod().unmodify(getModId());
355                //fleet.getStats().getFleetwideMaxBurnMod().unmodify(getModId());
356                //fleet.getStats().getAccelerationMult().unmodify(getModId());
357                //fleet.getCommanderStats().getDynamic().getStat(Stats.NAVIGATION_PENALTY_MULT).unmodify(getModId());
358                
359                primed = null;
360        }
361        
362
363        @Override
364        public boolean isUsable() {
365                return super.isUsable() && 
366                                        getFleet() != null;// && 
367                                        //getNonReadyShips().isEmpty();
368        }
369        
370//      protected List<FleetMemberAPI> getNonReadyShips() {
371//              List<FleetMemberAPI> result = new ArrayList<FleetMemberAPI>();
372//              CampaignFleetAPI fleet = getFleet();
373//              if (fleet == null) return result;
374//              
375//              float crCostFleetMult = fleet.getStats().getDynamic().getValue(Stats.EMERGENCY_BURN_CR_MULT);
376//              for (FleetMemberAPI member : fleet.getFleetData().getMembersListCopy()) {
377//                      //if (member.isMothballed()) continue;
378//                      float crLoss = member.getDeployCost() * CR_COST_MULT * crCostFleetMult;
379//                      if (Math.round(member.getRepairTracker().getCR() * 100) < Math.round(crLoss * 100)) {
380//                              result.add(member);
381//                      }
382//              }
383//              return result;
384//      }
385
386//      protected float computeSupplyCost() {
387//              CampaignFleetAPI fleet = getFleet();
388//              if (fleet == null) return 0f;
389//              
390//              float crCostFleetMult = fleet.getStats().getDynamic().getValue(Stats.EMERGENCY_BURN_CR_MULT);
391//              
392//              float cost = 0f;
393//              for (FleetMemberAPI member : fleet.getFleetData().getMembersListCopy()) {
394//                      cost += member.getDeploymentPointsCost() * CR_COST_MULT * crCostFleetMult;
395//              }
396//              return cost;
397//      }
398
399        
400        @Override
401        public void createTooltip(TooltipMakerAPI tooltip, boolean expanded) {
402                CampaignFleetAPI fleet = getFleet();
403                if (fleet == null) return;
404                
405                Color gray = Misc.getGrayColor();
406                Color highlight = Misc.getHighlightColor();
407                Color fuel = Global.getSettings().getColor("progressBarFuelColor");
408                Color bad = Misc.getNegativeHighlightColor();
409                
410                if (!Global.CODEX_TOOLTIP_MODE) {
411                        LabelAPI title = tooltip.addTitle("Interdiction Pulse");
412                } else {
413                        tooltip.addSpacer(-10f);
414                }
415
416                float pad = 10f;
417                
418                int range = (int) getRange(fleet);
419                
420                
421                tooltip.addPara("Slows* the fleet and uses its active sensor network to charge and release a powerful energy pulse that " +
422                                "can disrupt the drive fields of nearby fleets.", pad);
423                
424                Color c = Misc.getTooltipTitleAndLightHighlightColor();
425                Color hc = highlight;
426                if (Global.CODEX_TOOLTIP_MODE) hc = Misc.getBasePlayerColor();
427                tooltip.addPara("The disruption interrupts any movement-related abilities (such as %s or %s) " +
428                                "and prevents their use for some time afterwards. Also interrupts charging interdiction pulses.", pad,
429                                hc, "Sustained Burn", "Emergency Burn");
430
431                tooltip.addPara("The disruption lasts for %s seconds, modified by %s second for " +
432                                "every %s points of difference in the fleets' sensor strengths.", pad, highlight,
433                                "" + (int) BASE_SECONDS,
434                                "" + (int) 1,
435                                "" + (int) STRENGTH_PER_SECOND);
436                
437                tooltip.addPara("Base range of %s* units, increased by half your fleet's sensor strength, " +
438                                "for a total of %s units. While the pulse is charging, the range at which the fleet can be detected will " +
439                                "gradually increase by up to %s.", pad, highlight, 
440                                "" + (int) BASE_RANGE,
441                                "" + range,
442                                "" + (int) DETECTABILITY_PERCENT + "%");
443                
444                tooltip.addPara("A successful interdict is considered a hostile act, though not on the same level as " +
445                                                "open warfare.", pad);
446                
447                tooltip.addPara("*2000 units = 1 map grid cell", gray, pad);
448                tooltip.addPara("*A fleet is considered slow-moving at a burn level of half that of its slowest ship.", gray, pad);
449                addIncompatibleToTooltip(tooltip, expanded);
450        }
451
452        public boolean hasTooltip() {
453                return true;
454        }
455        
456
457        @Override
458        public void fleetLeftBattle(BattleAPI battle, boolean engagedInHostilities) {
459                if (engagedInHostilities) {
460                        deactivate();
461                }
462        }
463
464        @Override
465        public void fleetOpenedMarket(MarketAPI market) {
466                deactivate();
467        }
468        
469}
470
471
472
473
474