001package com.fs.starfarer.api.impl.campaign.intel.punitive;
002
003import java.util.ArrayList;
004import java.util.HashSet;
005import java.util.LinkedHashMap;
006import java.util.List;
007import java.util.Map;
008import java.util.Random;
009import java.util.Set;
010
011import org.json.JSONObject;
012
013import com.fs.starfarer.api.EveryFrameScript;
014import com.fs.starfarer.api.Global;
015import com.fs.starfarer.api.campaign.FactionAPI;
016import com.fs.starfarer.api.campaign.econ.CommodityMarketDataAPI;
017import com.fs.starfarer.api.campaign.econ.CommodityOnMarketAPI;
018import com.fs.starfarer.api.campaign.econ.Industry;
019import com.fs.starfarer.api.campaign.econ.MarketAPI;
020import com.fs.starfarer.api.impl.campaign.DebugFlags;
021import com.fs.starfarer.api.impl.campaign.ids.Factions;
022import com.fs.starfarer.api.impl.campaign.ids.Industries;
023import com.fs.starfarer.api.impl.campaign.ids.MemFlags;
024import com.fs.starfarer.api.impl.campaign.intel.BaseIntelPlugin;
025import com.fs.starfarer.api.impl.campaign.rulecmd.salvage.MarketCMD;
026import com.fs.starfarer.api.util.IntervalUtil;
027import com.fs.starfarer.api.util.Misc;
028import com.fs.starfarer.api.util.WeightedRandomPicker;
029
030public class PunitiveExpeditionManager implements EveryFrameScript {
031
032        public static final String KEY = "$core_punitiveExpeditionManager";
033        public static PunitiveExpeditionManager getInstance() {
034                Object test = Global.getSector().getMemoryWithoutUpdate().get(KEY);
035                return (PunitiveExpeditionManager) test; 
036        }
037        
038        public static int MAX_CONCURRENT = Global.getSettings().getInt("punExMaxConcurrent");
039        public static float PROB_TIMEOUT_PER_SENT = Global.getSettings().getFloat("punExProbTimeoutPerExpedition");
040        public static float MIN_TIMEOUT = Global.getSettings().getFloatFromArray("punExTimeoutDays", 0);
041        public static float MAX_TIMEOUT = Global.getSettings().getFloatFromArray("punExTimeoutDays", 1);
042        
043        public static int MIN_COLONY_SIZE_FOR_NON_TERRITORIAL = Global.getSettings().getInt("punExMinColonySizeForNonTerritorial");
044        
045        
046        // if more factions send non-territorial expeditions, longer timeout
047        public static float TARGET_NUMBER_FOR_FREQUENCY = 5f;
048        
049        public static float ANGER_BUILDUP_MULT = 0.5f;
050        
051        public static int FACTION_MUST_BE_IN_TOP_X_PRODUCERS = 3;
052        //public static float PLAYER_FRACTION_TO_NOTICE = 0.33f;
053        public static float PLAYER_FRACTION_TO_NOTICE = 0.5f;
054        //public static final float MAX_THRESHOLD = 1000f;
055        public static float MAX_THRESHOLD = 600f;
056        
057        public static enum PunExType {
058                ANTI_COMPETITION,
059                ANTI_FREE_PORT,
060                TERRITORIAL,
061        }
062        
063        public static enum PunExGoal {
064                RAID_PRODUCTION,
065                RAID_SPACEPORT,
066                BOMBARD,
067                //EVACUATE,
068        }
069        
070        public static class PunExReason {
071                public PunExType type;
072                public String commodityId;
073                public String marketId;
074                public float weight;
075                public PunExReason(PunExType type) {
076                        this.type = type;
077                }
078        }
079        
080        public static class PunExData {
081                public FactionAPI faction;
082                public IntervalUtil tracker = new IntervalUtil(20f, 40f);
083                public float anger = 0f;
084                public float threshold = 100f;
085                public float timeout = 0f;;
086                public BaseIntelPlugin intel;
087                public Random random = new Random();
088                
089                public int numSuccesses = 0;
090                public int numAttempts = 0;
091        }
092        
093        protected float timeout = 0f;
094        protected int numSentSinceTimeout = 0;
095        protected LinkedHashMap<FactionAPI, PunExData> data = new LinkedHashMap<FactionAPI, PunExData>();
096        
097        public PunitiveExpeditionManager() {
098                Global.getSector().getMemoryWithoutUpdate().set(KEY, this);
099        }
100        
101        protected Object readResolve() {
102                return this;
103        }
104        
105        public PunExData getDataFor(FactionAPI faction) {
106                return data.get(faction);
107        }
108        
109        
110        public LinkedHashMap<FactionAPI, PunExData> getData() {
111                return data;
112        }
113
114        public void advance(float amount) {
115                //if (true) return;
116                
117                float days = Misc.getDays(amount);
118                
119                Set<FactionAPI> seen = new HashSet<FactionAPI>();
120                for (MarketAPI market : Global.getSector().getEconomy().getMarketsInGroup(null)) {
121//                      JSONObject json = market.getFaction().getCustom().optJSONObject(Factions.CUSTOM_PUNITIVE_EXPEDITION_DATA);
122//                      boolean canSendWithoutMilitaryBase = json != null && json.optBoolean("canSendWithoutMilitaryBase", false);
123                        //if (market.getMemoryWithoutUpdate().getBoolean(MemFlags.MARKET_MILITARY) || canSendWithoutMilitaryBase) {
124                        if (true) {
125                                FactionAPI faction = market.getFaction();
126                                if (Misc.getCommissionFaction() == faction) continue;
127                                
128                                if (seen.contains(faction) || data.containsKey(faction)) {
129                                        seen.add(faction);
130                                        continue;
131                                }
132                                JSONObject json = faction.getCustomJSONObject(Factions.CUSTOM_PUNITIVE_EXPEDITION_DATA);
133                                if (json != null) {
134                                        PunExData curr = new PunExData();
135                                        curr.faction = faction;
136                                        data.put(faction, curr);
137                                        seen.add(faction);
138                                }
139                        }
140                }
141                data.keySet().retainAll(seen);
142
143                if (timeout > 0) {
144                        timeout -= days * (DebugFlags.PUNITIVE_EXPEDITION_DEBUG ? 1000f : 1f);
145                        if (timeout <= 0) {
146                                timeout = 0;
147                                numSentSinceTimeout = 0;
148                        }
149                        return;
150                }
151                
152                boolean first = true;
153                for (PunExData curr : data.values()) {
154                        if (first && DebugFlags.PUNITIVE_EXPEDITION_DEBUG) {
155                                days *= 1000f;
156                                curr.timeout = 0f;
157                                curr.anger = 1000f;
158                        }
159                        first = false;
160                        
161                        if (curr.intel != null) {
162                                if (curr.intel.isEnded()) {
163                                        curr.timeout = 100f + 100f * curr.random.nextFloat();
164                                        
165                                        if (curr.intel instanceof PunitiveExpeditionIntel) {
166                                                PunitiveExpeditionIntel intel = (PunitiveExpeditionIntel) curr.intel;
167                                                if (!intel.isTerritorial()) {
168                                                        curr.timeout += getExtraTimeout(curr);
169                                                }
170                                        }
171                                        
172                                        curr.intel = null;
173                                }
174                        } else {
175                                curr.timeout -= days;
176                                if (curr.timeout <= 0) curr.timeout = 0;
177                        }
178                        
179                        
180                        curr.tracker.advance(days);
181                        //System.out.println(curr.tracker.getElapsed());
182                        if (curr.tracker.intervalElapsed() && 
183                                        curr.intel == null && 
184                                        curr.timeout <= 0) {
185                                checkExpedition(curr);
186                        }
187                }
188        }
189        
190        public float getExtraTimeout(PunExData d) {
191                float total = 0f;
192                for (PunExData curr : data.values()) {
193                        JSONObject json = curr.faction.getCustom().optJSONObject(Factions.CUSTOM_PUNITIVE_EXPEDITION_DATA);
194                        if (json == null) continue;
195                        
196                        List<MarketAPI> markets = Misc.getFactionMarkets(curr.faction, null);
197                        if (markets.isEmpty()) continue;
198                        
199                        boolean vsCompetitors = json.optBoolean("vsCompetitors", false);
200                        boolean vsFreePort = json.optBoolean("vsFreePort", false);
201                        boolean territorial = json.optBoolean("territorial", false);
202                        
203                        if (vsCompetitors || vsFreePort) {
204                                total++;
205                        }
206                }
207                
208                return Math.min(10f, Math.max(0, total - TARGET_NUMBER_FOR_FREQUENCY)) * 
209                                        (MIN_TIMEOUT * 0.9f + MIN_TIMEOUT * 0.9f * d.random.nextFloat());
210        }
211        
212        
213        public int getOngoing() {
214                int ongoing = 0;
215                for (PunExData d : data.values()) {
216                        if (d.intel != null) {
217                                ongoing++;
218                        }
219                }
220                //ongoing = 0;
221                return ongoing;
222        }
223
224        protected void checkExpedition(PunExData curr) {
225                JSONObject json = curr.faction.getCustom().optJSONObject(Factions.CUSTOM_PUNITIVE_EXPEDITION_DATA);
226                if (json == null) return;
227                
228//              if (curr.faction.getId().equals(Factions.TRITACHYON)) {
229//                      System.out.println("wefwefwe");
230//              }
231                List<PunExReason> reasons = getExpeditionReasons(curr);
232//              if (!reasons.isEmpty()) {
233//                      System.out.println("HERE");
234//              }
235                float total = 0f;
236                for (PunExReason reason : reasons) {
237                        total += reason.weight;
238                }
239                
240                total *= ANGER_BUILDUP_MULT;
241                
242                curr.anger += total * (0.25f + curr.random.nextFloat() * 0.75f);
243                if (curr.anger >= curr.threshold) {
244                        if (getOngoing() >= MAX_CONCURRENT) {
245                                curr.anger = 0;
246                        } else {
247                                createExpedition(curr);
248                        }
249                }
250        }
251        
252        public static float COMPETITION_PRODUCTION_MULT = 20f;
253        public static float ILLEGAL_GOODS_MULT = 3f;
254        public static float FREE_PORT_SIZE_MULT = 5f;
255        public static float TERRITORIAL_ANGER = 500f;
256        
257        public List<PunExReason> getExpeditionReasons(PunExData curr) {
258                List<PunExReason> result = new ArrayList<PunExReason>();
259
260                JSONObject json = curr.faction.getCustom().optJSONObject(Factions.CUSTOM_PUNITIVE_EXPEDITION_DATA);
261                if (json == null) return result;
262                
263                List<MarketAPI> markets = Misc.getFactionMarkets(curr.faction, null);
264                if (markets.isEmpty()) return result;
265                
266                boolean vsCompetitors = json.optBoolean("vsCompetitors", false);
267                boolean vsFreePort = json.optBoolean("vsFreePort", false);
268                boolean territorial = json.optBoolean("territorial", false);
269                
270                MarketAPI test = markets.get(0);
271                FactionAPI player = Global.getSector().getPlayerFaction();
272                
273                if (vsCompetitors) {
274                        for (CommodityOnMarketAPI com : test.getAllCommodities()) {
275                                if (com.isNonEcon()) continue;
276                                if (curr.faction.isIllegal(com.getId())) continue;
277                                
278                                CommodityMarketDataAPI cmd = com.getCommodityMarketData();
279                                if (cmd.getMarketValue() <= 0) continue;
280                                
281                                Map<FactionAPI, Integer> shares = cmd.getMarketSharePercentPerFaction();
282                                int numHigher = 0;
283                                int factionShare = shares.get(curr.faction);
284                                if (factionShare <= 0) continue;
285                                
286                                for (FactionAPI faction : shares.keySet()) {
287                                        if (curr.faction == faction) continue;
288                                        if (shares.get(faction) > factionShare) {
289                                                numHigher++;
290                                        }
291                                }
292                                
293                                if (numHigher >= FACTION_MUST_BE_IN_TOP_X_PRODUCERS) continue;
294                                
295                                int playerShare = cmd.getMarketSharePercent(player);
296                                float threshold = PLAYER_FRACTION_TO_NOTICE;
297                                if (DebugFlags.PUNITIVE_EXPEDITION_DEBUG) {
298                                        threshold = 0.1f;
299                                }
300                                if (playerShare < factionShare * threshold || playerShare <= 0) continue;
301                                
302                                PunExReason reason = new PunExReason(PunExType.ANTI_COMPETITION);
303                                reason.weight = (float)playerShare / (float)factionShare * COMPETITION_PRODUCTION_MULT;
304                                reason.commodityId = com.getId();
305                                result.add(reason);
306                        }
307                }
308                
309                if (vsFreePort) {
310                        for (MarketAPI market : Global.getSector().getEconomy().getMarketsInGroup(null)) {
311                                if (!market.isPlayerOwned()) continue;
312                                if (!market.isFreePort()) continue;
313                                if (market.isInHyperspace()) continue;
314                                
315                                for (CommodityOnMarketAPI com : test.getAllCommodities()) {
316                                        if (com.isNonEcon()) continue;
317                                        if (!curr.faction.isIllegal(com.getId())) continue;
318                                        
319                                        CommodityMarketDataAPI cmd = com.getCommodityMarketData();
320                                        if (cmd.getMarketValue() <= 0) continue;
321                                        
322                                        int playerShare = cmd.getMarketSharePercent(player);
323                                        if (playerShare <= 0) continue;
324                                        
325                                        PunExReason reason = new PunExReason(PunExType.ANTI_FREE_PORT);
326                                        reason.weight = playerShare * ILLEGAL_GOODS_MULT;
327                                        reason.commodityId = com.getId();
328                                        reason.marketId = market.getId();
329                                        result.add(reason);
330                                }
331                                
332                                if (market.isFreePort()) {
333                                        PunExReason reason = new PunExReason(PunExType.ANTI_FREE_PORT);
334                                        reason.weight = Math.max(1, market.getSize() - 2) * FREE_PORT_SIZE_MULT;
335                                        reason.marketId = market.getId();
336                                        result.add(reason);
337                                }
338                        }
339                }
340                
341                if (territorial) {
342                        int maxSize = MarketCMD.getBombardDestroyThreshold();
343                        for (MarketAPI market : Global.getSector().getEconomy().getMarketsInGroup(null)) {
344                                if (!market.isPlayerOwned()) continue;
345                                if (market.isInHyperspace()) continue;
346                                
347                                boolean destroy = market.getSize() <= maxSize;
348                                if (!destroy) continue;
349                                
350                                FactionAPI claimedBy = Misc.getClaimingFaction(market.getPrimaryEntity());
351                                if (claimedBy != curr.faction) continue;
352                                
353                                PunExReason reason = new PunExReason(PunExType.TERRITORIAL);
354                                reason.weight = TERRITORIAL_ANGER;
355                                reason.marketId = market.getId();
356                                result.add(reason);
357                        }
358                }
359                
360                return result;
361        }
362        
363
364        public void createExpedition(PunExData curr) {
365                createExpedition(curr, null);
366        }
367        public void createExpedition(PunExData curr, Integer fpOverride) {
368                
369                JSONObject json = curr.faction.getCustom().optJSONObject(Factions.CUSTOM_PUNITIVE_EXPEDITION_DATA);
370                if (json == null) return;
371                
372//              boolean vsCompetitors = json.optBoolean("vsCompetitors", false);
373//              boolean vsFreePort = json.optBoolean("vsFreePort", false);
374                boolean canBombard = json.optBoolean("canBombard", false);
375//              boolean territorial = json.optBoolean("territorial", false);
376                
377                List<PunExReason> reasons = getExpeditionReasons(curr);
378                WeightedRandomPicker<PunExReason> reasonPicker = new WeightedRandomPicker<PunExReason>(curr.random);
379                for (PunExReason r : reasons) {
380                        //if (r.type == PunExType.ANTI_COMPETITION) continue;
381                        reasonPicker.add(r, r.weight);
382                }
383                PunExReason reason = reasonPicker.pick();
384                if (reason == null) return;
385                
386                
387                WeightedRandomPicker<MarketAPI> targetPicker = new WeightedRandomPicker<MarketAPI>(curr.random);
388                //for (PunExReason reason : reasons) {
389                
390                //WeightedRandomPicker<MarketAPI> picker = new WeightedRandomPicker<MarketAPI>(curr.random);
391                for (MarketAPI market : Global.getSector().getEconomy().getMarketsCopy()) {
392                        if (!market.isPlayerOwned()) continue;
393                        if (market.isInHyperspace()) continue;
394                        
395                        float weight = 0f;
396                        if (reason.type == PunExType.ANTI_COMPETITION && reason.commodityId != null) {
397                                if (market.getSize() < MIN_COLONY_SIZE_FOR_NON_TERRITORIAL) continue;
398                                
399                                CommodityOnMarketAPI com = market.getCommodityData(reason.commodityId);
400                                int share = com.getCommodityMarketData().getExportMarketSharePercent(market);
401//                              if (share <= 0 && com.getAvailable() > 0) {
402//                                      share = 1;
403//                              }
404                                weight += share * share;
405                        } else if (reason.type == PunExType.ANTI_FREE_PORT && market.getId().equals(reason.marketId)) {
406                                if (market.getSize() < MIN_COLONY_SIZE_FOR_NON_TERRITORIAL) continue;
407                                
408                                weight = 1f;
409                        } else if (reason.type == PunExType.TERRITORIAL && market.getId().equals(reason.marketId)) {
410                                weight = 1f;
411                        }
412                        
413                        targetPicker.add(market, weight);
414                }
415                
416                MarketAPI target = targetPicker.pick();
417                if (target == null) return;
418                
419                WeightedRandomPicker<MarketAPI> picker = new WeightedRandomPicker<MarketAPI>(curr.random);
420                for (MarketAPI market : Global.getSector().getEconomy().getMarketsInGroup(null)) {
421                        boolean canSendWithoutMilitaryBase = json.optBoolean("canSendWithoutMilitaryBase", false);
422                        boolean military = market.getMemoryWithoutUpdate().getBoolean(MemFlags.MARKET_MILITARY); 
423                        if (market.getFaction() == curr.faction && 
424                                        (military || canSendWithoutMilitaryBase)) {
425                                float w = 1f;
426                                if (military) w *= 10f;
427                                picker.add(market, market.getSize() * w);
428                        }
429                }
430                
431                MarketAPI from = picker.pick();
432                if (from == null) return;
433                
434                PunExGoal goal = null;
435                Industry industry = null;
436                if (reason.type == PunExType.ANTI_FREE_PORT) {
437                        goal = PunExGoal.RAID_SPACEPORT;
438                        if (canBombard && curr.numSuccesses >= 2) {
439                                goal = PunExGoal.BOMBARD;
440                        }
441                } else if (reason.type == PunExType.TERRITORIAL) {
442                        if (canBombard || true) {
443                                goal = PunExGoal.BOMBARD;
444                        } else {
445                                //goal = PunExGoal.EVACUATE;
446                        }
447                } else {
448                        goal = PunExGoal.RAID_PRODUCTION;
449                        if (reason.commodityId == null || curr.numSuccesses >= 1) {
450                                goal = PunExGoal.RAID_SPACEPORT;
451                        }
452                        if (canBombard && curr.numSuccesses >= 2) {
453                                goal = PunExGoal.BOMBARD;
454                        }
455                }
456                
457                //goal = PunExGoal.BOMBARD;
458                
459                if (goal == PunExGoal.RAID_SPACEPORT) {
460                        for (Industry temp : target.getIndustries()) {
461                                if (temp.getSpec().hasTag(Industries.TAG_UNRAIDABLE)) continue;
462                                if (temp.getSpec().hasTag(Industries.TAG_SPACEPORT)) {
463                                        industry = temp;
464                                        break;
465                                }
466                        }
467                        if (industry == null) return;
468                } else if (goal == PunExGoal.RAID_PRODUCTION && reason.commodityId != null) {
469                        int max = 0;
470                        for (Industry temp : target.getIndustries()) {
471                                if (temp.getSpec().hasTag(Industries.TAG_UNRAIDABLE)) continue;
472                                
473                                int prod = temp.getSupply(reason.commodityId).getQuantity().getModifiedInt();
474                                if (prod > max) {
475                                        max = prod;
476                                        industry = temp;
477                                }
478                        }
479                        if (industry == null) return;
480                }
481                
482                //float fp = from.getSize() * 20 + threshold * 0.5f;
483                float fp = 50 + curr.threshold * 0.5f;
484                fp = Math.max(50, fp - 50);
485                //fp = 500;
486//              if (from.getFaction().isHostileTo(target.getFaction())) {
487//                      fp *= 1.25f;
488//              }
489                
490                if (fpOverride != null) {
491                        fp = fpOverride;
492                }
493                
494
495                float totalAttempts = 0f;
496                for (PunExData d : data.values()) {
497                        totalAttempts += d.numAttempts;
498                }
499                //if (totalAttempts > 10) totalAttempts = 10;
500                
501                float extraMult = 0f;
502                if (totalAttempts <= 2) {
503                        extraMult = 0f;
504                } else if (totalAttempts <= 4) {
505                        extraMult = 1f;
506                } else if (totalAttempts <= 7) {
507                        extraMult = 2f;
508                } else if (totalAttempts <= 10) {
509                        extraMult = 3f;
510                } else {
511                        extraMult = 4f;
512                }
513                
514                float orgDur = 20f + extraMult * 10f + (10f + extraMult * 5f) * (float) Math.random();
515                
516                
517                curr.intel = new PunitiveExpeditionIntel(from.getFaction(), from, target, fp, orgDur,
518                                                                                                 goal, industry, reason);
519                if (curr.intel.isDone()) {
520                        curr.intel = null;
521                        timeout = orgDur + MIN_TIMEOUT + curr.random.nextFloat() * (MAX_TIMEOUT - MIN_TIMEOUT);
522                        return;
523                }
524                
525                if (curr.random.nextFloat() < numSentSinceTimeout * PROB_TIMEOUT_PER_SENT) {
526                        timeout = orgDur + MIN_TIMEOUT + curr.random.nextFloat() * (MAX_TIMEOUT - MIN_TIMEOUT);
527                }
528                numSentSinceTimeout++;
529                
530                curr.numAttempts++;
531                curr.anger = 0f;
532                curr.threshold *= 2f;
533                if (curr.threshold > MAX_THRESHOLD) {
534                        curr.threshold = MAX_THRESHOLD;
535                }
536        }
537        
538        
539
540        public boolean isDone() {
541                return false;
542        }
543
544        public boolean runWhilePaused() {
545                return false;
546        }
547        
548}
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563