001package com.fs.starfarer.api.impl.campaign.skills;
002
003import java.util.ArrayList;
004import java.util.LinkedHashMap;
005import java.util.List;
006import java.util.Map;
007
008import java.awt.Color;
009
010import org.lwjgl.input.Mouse;
011
012import com.fs.starfarer.api.Global;
013import com.fs.starfarer.api.characters.PersonAPI;
014import com.fs.starfarer.api.combat.BaseEveryFrameCombatPlugin;
015import com.fs.starfarer.api.combat.CombatEngineAPI;
016import com.fs.starfarer.api.combat.CombatFleetManagerAPI;
017import com.fs.starfarer.api.combat.DeployedFleetMemberAPI;
018import com.fs.starfarer.api.combat.ShipAIPlugin;
019import com.fs.starfarer.api.combat.ShipAPI;
020import com.fs.starfarer.api.combat.ShipCommand;
021import com.fs.starfarer.api.combat.ViewportAPI;
022import com.fs.starfarer.api.combat.WeaponGroupAPI;
023import com.fs.starfarer.api.impl.campaign.ids.Stats;
024import com.fs.starfarer.api.impl.hullmods.NeuralInterface;
025import com.fs.starfarer.api.input.InputEventAPI;
026import com.fs.starfarer.api.loading.WeaponGroupSpec;
027import com.fs.starfarer.api.mission.FleetSide;
028
029public class NeuralLinkScript extends BaseEveryFrameCombatPlugin {
030        public static String TRANSFER_CONTROL = "SHIP_TOGGLE_XPAN_MODE";
031        
032        public static float INSTANT_TRANSFER_DP = 50;
033        public static float TRANSFER_SECONDS_PER_DP = 0.125f;
034        public static float TRANSFER_MAX_SECONDS = 5f;
035        public static float TRANSFER_MIN_SECONDS_IF_NOT_INSTANT = 1f;
036        
037        // doesn't really work out - disables shields etc, and you also can't just
038        // switch to see how the ship is doing without really disrupting it
039        public static boolean ALLOW_ENGINE_CONTROL_DURING_TRANSFER = false;
040        
041        public static final Object KEY_STATUS = new Object();
042        public static final Object KEY_STATUS2 = new Object();
043        
044        public static final String TRANSFER_COMPLETE_KEY = "neural_transfer_complete_key";
045        
046        
047        public static class SavedShipControlState {
048                public ShipAPI ship;
049                public Map<WeaponGroupAPI, Boolean> autofiring = new LinkedHashMap<WeaponGroupAPI, Boolean>();
050                public WeaponGroupAPI selected;
051        }
052        
053        
054        
055        protected CombatEngineAPI engine;
056        public void init(CombatEngineAPI engine) {
057                this.engine = engine;
058        }
059        
060        protected ShipAPI prevPlayerShip = null;
061        protected int skipFrames = 0;
062        
063        protected List<ShipAPI> linked = new ArrayList<ShipAPI>();
064        
065        protected float untilTransfer;
066        protected int lastShownTime = 0;
067        
068        protected SavedShipControlState prevState;
069        protected SavedShipControlState savedState;
070        
071        protected PersonAPI playerPerson;
072
073        public void saveControlState(ShipAPI ship) {
074                prevState = savedState;
075                savedState = new SavedShipControlState();
076                savedState.ship = ship;
077                for (WeaponGroupAPI group : ship.getWeaponGroupsCopy()) {
078                        savedState.autofiring.put(group, group.isAutofiring());
079                }
080                savedState.selected = ship.getSelectedGroupAPI();
081        }
082        public void restoreControlState(ShipAPI ship) {
083                if (ship == null) {
084                        return;
085                }
086                
087                if (prevState == null || prevState.ship != ship) {
088                        List<WeaponGroupAPI> groups = ship.getWeaponGroupsCopy();
089                        int index = 0;
090                        for (WeaponGroupSpec groupSpec : ship.getVariant().getWeaponGroups()) {
091                                if (index >= groups.size()) break;
092                                
093                                boolean auto = groupSpec.isAutofireOnByDefault();
094                                WeaponGroupAPI group = groups.get(index);
095                                if (group != null) {
096                                        if (auto) {
097                                                group.toggleOn();
098                                        } else {
099                                                group.toggleOff();
100                                        }
101                                }
102                                index++;
103                        }
104                        if (groups.size() >= 1) {
105                                ship.giveCommand(ShipCommand.SELECT_GROUP, null, 0);
106                        }
107                        return;
108                }
109                
110                
111                for (WeaponGroupAPI group : ship.getWeaponGroupsCopy()) {
112                        Boolean auto = prevState.autofiring.get(group);
113                        if (auto == null) auto = false;
114                        if (auto) {
115                                group.toggleOn();
116                        } else {
117                                group.toggleOff();
118                        }
119                }
120                int index = ship.getWeaponGroupsCopy().indexOf(prevState.selected);
121                if (index > 0) {
122                        ship.giveCommand(ShipCommand.SELECT_GROUP, null, index);
123                }
124        }
125        
126        public void advance(float amount, List<InputEventAPI> events) {
127                if (engine == null) return;
128                if (engine.isPaused()) return;
129                
130                
131                ShipAPI playerShip = engine.getPlayerShip();
132                if (playerShip == null) {
133                        return;
134                }
135
136                if (!playerShip.isAlive()) {
137                        untilTransfer = 0f;
138                        lastShownTime = 0;
139                }
140                
141                if (untilTransfer > 0) {
142                        float timeMult = playerShip.getMutableStats().getTimeMult().getModifiedValue();
143                        untilTransfer -= amount * timeMult;
144                        
145                        Global.getSoundPlayer().applyLowPassFilter(0.75f, 0f);
146//                      Global.getSoundPlayer().applyLowPassFilter(1f - (1f - spec.getFilterGain()) * level * mult, 
147//                                      1f - (1f - spec.getFilterGainHF()) * level * mult);
148                        //engine.getCombatUI().setShipInfoFanOutBrightness(0f);
149                        engine.getCombatUI().hideShipInfo();
150                        if (untilTransfer <= 0) {
151                                untilTransfer = 0;
152                                Global.getSoundPlayer().playUISound("ui_neural_transfer_complete", 1f, 1f);
153                                playerShip.setCustomData(TRANSFER_COMPLETE_KEY, true);
154                                showTranferFloatyIfNeeded();
155                                engine.getCombatUI().reFanOutShipInfo();
156                                boolean autopilot = engine.getCombatUI().isAutopilotOn();
157                                if (autopilot) {
158                                        if (playerShip.getAI() == null) { // somehow?
159                                                CombatFleetManagerAPI manager = engine.getFleetManager(FleetSide.PLAYER);
160                                                DeployedFleetMemberAPI member = manager.getDeployedFleetMember(playerShip);
161                                                playerShip.setShipAI(Global.getSettings().pickShipAIPlugin(member == null ? null : member.getMember(), playerShip));
162                                        }
163                                } else {
164                                        playerShip.setShipAI(null);
165                                        restoreControlState(playerShip);
166                                }
167                        } else {
168                                suppressControlsDuringTransfer(playerShip);
169                                showTranferFloatyIfNeeded();
170                        }
171                }
172                
173                // if the player changed flagships:
174                // skip a few frames to make sure the status ends up on top of the status list
175                if (playerShip != prevPlayerShip) {
176                        prevPlayerShip = playerShip;
177                        skipFrames = 30;
178                }
179                
180                if (skipFrames > 0) {
181                        skipFrames--;
182                        return;
183                }
184                
185                updateLinkState();
186        }
187        
188        public void suppressControlsDuringTransfer(ShipAPI playerShip) {
189                if (ALLOW_ENGINE_CONTROL_DURING_TRANSFER) {
190                        playerShip.blockCommandForOneFrame(ShipCommand.FIRE);
191                        playerShip.blockCommandForOneFrame(ShipCommand.TOGGLE_AUTOFIRE);
192                        playerShip.blockCommandForOneFrame(ShipCommand.PULL_BACK_FIGHTERS);
193                        playerShip.blockCommandForOneFrame(ShipCommand.VENT_FLUX);
194                        playerShip.blockCommandForOneFrame(ShipCommand.USE_SELECTED_GROUP);
195                        playerShip.blockCommandForOneFrame(ShipCommand.USE_SYSTEM);
196                        playerShip.blockCommandForOneFrame(ShipCommand.HOLD_FIRE);
197                } else {
198                        engine.getCombatUI().setDisablePlayerShipControlOneFrame(true);
199                }
200        }
201        
202        public void showTranferFloatyIfNeeded() {
203                ShipAPI playerShip = engine.getPlayerShip();
204                if (playerShip == null) return;
205                float timeMult = playerShip.getMutableStats().getTimeMult().getModifiedValue();
206                Color color = new Color(0,121,216,255);
207                
208// feels kind of annoying               
209//              if (untilTransfer <= 0) {
210//                      engine.addFloatingTextAlways(playerShip.getLocation(), "Transfer complete",
211//                                      getFloatySize(playerShip), color, playerShip, 5f * timeMult, 2f, 1f/timeMult, 0f, 0f,
212//                                      1f);
213//                      return;
214//              }
215                
216                int show = (int) Math.ceil(untilTransfer);
217                if (show != lastShownTime) {
218                        if (show > 0) {
219                                engine.addFloatingTextAlways(playerShip.getLocation(), "Neural transfer in " + show,
220                                                getFloatySize(playerShip), color, playerShip, 4f * timeMult, 0.8f/timeMult, 1f/timeMult, 0f, 0f,
221                                                1f);
222                        }
223                        lastShownTime = show;
224                        //Global.getSoundPlayer().playUISound("ui_hold_fire_on", 2f, 0.25f);
225                }
226        }
227        
228        public boolean canLink(ShipAPI ship) {
229                // below line: debug, work for every ship
230                //if (ship.isAlive() && !ship.isShuttlePod()) return true;
231                
232                ShipAPI playerShip = engine.getPlayerShip();
233                // transferred command to officer'ed ship, can't link
234                if (ship == playerShip && ship.getOriginalCaptain() != null && 
235                                !ship.getOriginalCaptain().isDefault() && !ship.getOriginalCaptain().isPlayer()) {
236                        return false;
237                }
238                
239                if (engine.isInCampaign() || engine.isInCampaignSim()) {
240                        if (Global.getSector().getPlayerStats().getDynamic().getMod(Stats.HAS_NEURAL_LINK).computeEffective(0f) <= 0) {
241                                return false;
242                        }
243                }
244                
245                boolean aliveOrDisabledButNonPhysical = ship.isAlive();
246                ShipAPI physicalLocation = engine.getShipPlayerLastTransferredCommandTo();
247                if (ship == playerShip && ship != physicalLocation) {
248                        aliveOrDisabledButNonPhysical = true;
249                }
250                                                
251                return aliveOrDisabledButNonPhysical && !ship.isShuttlePod() &&
252                           ship.getMutableStats().getDynamic().getMod(Stats.HAS_NEURAL_LINK).computeEffective(0f) > 0;
253                           //ship.getVariant().hasHullMod(HullMods.NEURAL_INTERFACE);
254        }
255        
256        public void updateLinkState() {
257                ShipAPI playerShip = engine.getPlayerShip();
258                if (playerShip == null) return;
259                
260                ShipAPI physicalLocation = engine.getShipPlayerLastTransferredCommandTo();
261                for (ShipAPI ship : new ArrayList<ShipAPI>(linked)) {
262                        if (!ship.isAlive()) {
263                                if (ship == playerShip && ship != physicalLocation) {
264                                        continue;
265                                }
266                                
267                                if (ship != playerShip) {
268                                        PersonAPI orig = ship.getOriginalCaptain();
269                                        if (orig.isPlayer()) {
270                                                orig = Global.getFactory().createPerson();
271                                                if (engine.isInCampaign() || engine.isInCampaignSim()) {
272                                                        orig.setPersonality(Global.getSector().getPlayerFaction().pickPersonality());
273                                                }
274                                        }
275                                        ship.setCaptain(orig);
276                                }
277                                linked.remove(ship);
278                        }
279                }
280                
281                boolean physicallyPresent = linked.contains(physicalLocation);
282                if (!linked.contains(playerShip) || !physicallyPresent ||
283                                !canLink(playerShip)) {
284                        for (ShipAPI ship : linked) {
285                                PersonAPI orig = ship.getOriginalCaptain();
286                                if (orig.isPlayer()) {
287                                        orig = Global.getFactory().createPerson();
288                                        if (engine.isInCampaign() || engine.isInCampaignSim()) {
289                                                orig.setPersonality(Global.getSector().getPlayerFaction().pickPersonality());
290                                        }
291                                }
292                                if (ship.getCaptain() != orig) {
293                                        ship.setCaptain(orig);
294                                        if (ship.getFleetMember() != null) {
295                                                ship.getFleetMember().setCaptain(ship.getOriginalCaptain());
296                                        }
297                                }
298                        }
299                        linked.clear();
300                        if (canLink(playerShip)) {
301                                linked.add(playerShip);
302                        }
303                }
304                
305                if (linked.isEmpty()) return;
306                
307                
308                CombatFleetManagerAPI manager = engine.getFleetManager(FleetSide.PLAYER);
309                List<DeployedFleetMemberAPI> members = manager.getDeployedCopyDFM();
310                
311                if (physicallyPresent) {
312                        for (DeployedFleetMemberAPI dfm : members) {
313                                if (linked.size() >= 2) break;
314                                
315                                if (dfm.isFighterWing()) continue;
316                                if (dfm.isAlly()) continue;
317                                
318                                ShipAPI ship = dfm.getShip();
319                                if (linked.contains(ship)) continue;
320                                if (!ship.getCaptain().isDefault() && ship != playerShip &&
321                                                !ship.getCaptain().isPlayer()) {
322                                        // this last for when the player deploys a ship with NI in the simulator and then
323                                        // deploys their actual flagship - so, the player ship doesn't *actually* have the player in it
324                                        // but the other ship does
325                                        continue;
326                                }
327                                
328                                // transferred command to an officer'ed ship, no link
329                                if (ship == playerShip && ship.getOriginalCaptain() != null && 
330                                                !ship.getOriginalCaptain().isDefault() &&
331                                                !ship.getOriginalCaptain().isPlayer()) {
332                                        continue;
333                                }
334                                if (ship.controlsLocked()) continue;
335                                
336                                if (canLink(ship)) {
337                                        linked.add(ship);
338                                }
339                        }
340                }
341                
342                PersonAPI player = playerPerson; 
343                if (player == null) {
344                        player = playerShip.getCaptain();
345                        if (!player.isDefault() && playerPerson == null) {
346                                playerPerson = player;
347                        }
348                }
349                
350                for (ShipAPI ship : linked) {
351                        if (ship.getCaptain() != player) {
352                                ship.setCaptain(player);
353                                if (ship.getFleetMember() != null) {
354                                        ship.getFleetMember().setCaptain(player);
355                                }
356                        }
357                }
358                
359                if (linked.contains(playerShip)) {
360                        ShipAPI other = null;
361                        for (ShipAPI ship : linked) {
362                                if (ship != playerShip) {
363                                        other = ship;
364                                        break;
365                                }
366                        }
367                        
368                        String title = "Neural System Reset";
369                        //String title = "System Reset on Transfer";
370                        String icon = Global.getSettings().getSpriteName("ui", "icon_neural_link");
371                        String key = NeuralInterface.SYSTEM_RESET_TIMEOUT_KEY;
372//                      Float timeout = (Float) Global.getCombatEngine().getCustomData().get(key);
373//                      if (timeout == null) timeout = 0f;
374                        Float timeout = null;
375                        if (other != null) timeout = (Float) other.getCustomData().get(key);
376                        if (timeout == null) timeout = 0f;
377                        if (other == null) {
378                                engine.maintainStatusForPlayerShip(KEY_STATUS2, icon, title, "No signal", true);
379                        } else if (timeout <= 0) {
380                                engine.maintainStatusForPlayerShip(KEY_STATUS2, icon, title, "Ready on transfer", false);
381                        } else {
382                                int show = (int) Math.ceil(timeout);
383                                engine.maintainStatusForPlayerShip(KEY_STATUS2, icon, title, "Ready in " + show + " seconds", true);
384                        }
385                }
386                
387                for (ShipAPI ship : linked) {
388                        if (ship != playerShip) {
389                                
390                                if (untilTransfer <= 0f) {
391                                        String title = "Neural Link Active";
392                                        //String data = ship.getName() + ", " + ship.getHullSpec().getHullNameWithDashClass();
393                                        //String data = ship.getName() + ", " + ship.getHullSpec().getHullName();
394                                        String data = "Target: " + ship.getHullSpec().getHullNameWithDashClass();
395                                        String icon = Global.getSettings().getSpriteName("ui", "icon_neural_link");
396                                        engine.maintainStatusForPlayerShip(KEY_STATUS, icon, title, data, false);
397                                } else {
398                                        int show = (int) Math.ceil(untilTransfer);
399                                        if (show > 0) {
400                                                String title = "Neural Transfer";
401                                                String data = "Link in " + show + " seconds";
402                                                String icon = Global.getSettings().getSpriteName("ui", "icon_neural_link");
403                                                engine.maintainStatusForPlayerShip(KEY_STATUS, icon, title, data, true);
404                                        }
405                                }
406                                
407                                break;
408                        }
409                }
410                
411                if (linked.size() <= 1 && linked.contains(playerShip)) {
412                        String title = "Neural Link Inactive";
413                        String data = "No signal";
414                        if (!physicallyPresent) {
415                                data = "requires physical transfer";
416                        }
417                        String icon = Global.getSettings().getSpriteName("ui", "icon_neural_link");
418                        engine.maintainStatusForPlayerShip(KEY_STATUS, icon, title, data, true);
419                }
420        }
421        
422        
423        public void processInputPreCoreControls(float amount, List<InputEventAPI> events) {
424                if (engine == null || engine.getCombatUI() == null || engine.getCombatUI().isShowingCommandUI()) return;
425                
426                ShipAPI playerShip = engine.getPlayerShip();
427                if (playerShip == null) return;
428                if (!linked.contains(playerShip) || linked.size() < 2) return;
429                //if (untilTransfer > 0) return;
430                
431                for (InputEventAPI event : events) {
432                        if (event.isConsumed()) continue;
433                        
434                        if (event.isControlDownEvent(TRANSFER_CONTROL)) {
435                                if (untilTransfer <= 0) {
436                                        for (ShipAPI ship : linked) {
437                                                if (ship != playerShip) {
438                                                        untilTransfer = getTransferTime();
439                                                        lastShownTime = 0;
440                                                        doTransfer(ship);
441                                                }
442                                        }
443                                }
444                                event.consume();
445                                return;
446                        }
447                }
448        }
449        
450        
451        public void doTransfer(ShipAPI ship) {
452                if (ship == null) return;
453                ShipAPI playerShip = engine.getPlayerShip();
454                if (playerShip == null) return;
455                if (!linked.contains(playerShip)) return;
456                if (!linked.contains(ship)) return;
457                
458                if (untilTransfer <= 0) {
459                        Global.getSoundPlayer().playUISound("ui_neural_transfer_complete", 1f, 1f);
460                        ship.setCustomData(TRANSFER_COMPLETE_KEY, true);
461                        showTranferFloatyIfNeeded();
462                        engine.getCombatUI().reFanOutShipInfo();
463                } else {
464                        Global.getSoundPlayer().playUISound("ui_neural_transfer_begin", 1f, 1f);
465                }
466                
467                // I have it on good authority that this looks bad.
468                // "[it looks like] some vague energy field effect, then like Troi would have a headache and some dubious writing would occur"
469//              float animDur = Math.max(untilTransfer, 0.5f);
470//              engine.addLayeredRenderingPlugin(new NeuralTransferVisual(playerShip, ship, animDur));
471                
472                Mouse.setCursorPosition((int)Global.getSettings().getScreenWidthPixels()/2,
473                                                                (int)Global.getSettings().getScreenHeightPixels()/2);
474                
475                saveControlState(playerShip);
476                
477                ShipAIPlugin playerShipAI = playerShip.getShipAI(); // non-null if autopilot is on
478                ShipAIPlugin prevTargetAI = ship.getShipAI();
479                engine.setPlayerShipExternal(ship);
480                if (ship.getFleetMember() != null) {
481                        ship.getFleetMember().setCaptain(playerShip.getCaptain());
482                }
483                
484                if (playerShipAI != null) {
485                        playerShip.setShipAI(playerShipAI); // not the player ship anymore, the old ship
486                }
487                
488                boolean autopilot = engine.getCombatUI().isAutopilotOn();
489                
490                if (untilTransfer > 0) {
491                        if (!ALLOW_ENGINE_CONTROL_DURING_TRANSFER) {
492                                ship.setShipAI(prevTargetAI);
493                        }
494                        //engine.getCombatUI().setDisablePlayerShipControlOneFrame(true);
495                        suppressControlsDuringTransfer(ship);
496                        showTranferFloatyIfNeeded();
497                } else if (autopilot) {
498                        CombatFleetManagerAPI manager = engine.getFleetManager(FleetSide.PLAYER);
499                        DeployedFleetMemberAPI member = manager.getDeployedFleetMember(ship);
500                        ship.setShipAI(Global.getSettings().pickShipAIPlugin(member == null ? null : member.getMember(), ship));
501                }
502                
503                if (untilTransfer <= 0) {
504                        showTranferFloatyIfNeeded();
505                        if (!autopilot) {
506                                restoreControlState(ship);
507                        }
508                }
509        }
510
511        
512        public float getTransferTime() {
513                float total = 0f;
514                boolean instant = false;
515                for (ShipAPI ship : linked) {
516                        if (ship.getFleetMember() == null) continue;
517                        total += ship.getFleetMember().getDeploymentPointsCost();
518                        if (ship == Global.getCombatEngine().getPlayerShip() &&
519                                        ship.getMutableStats().getDynamic().getValue(Stats.INSTANT_NEURAL_TRANSFER_FROM, 0f) > 0f) {
520                                instant = true;
521                        }
522                }
523                
524                total = Math.round(total);
525                if (instant) total = 0f;
526                
527                //INSTANT_TRANSFER_DP = 0f;
528                
529                if (total <= INSTANT_TRANSFER_DP) return 0f;
530                
531                float time = (total - INSTANT_TRANSFER_DP) * TRANSFER_SECONDS_PER_DP;
532                if (time < TRANSFER_MIN_SECONDS_IF_NOT_INSTANT) {
533                        time = TRANSFER_MIN_SECONDS_IF_NOT_INSTANT;
534                } else if (time > TRANSFER_MAX_SECONDS) {
535                        time = TRANSFER_MAX_SECONDS;
536                }
537                time = (float) Math.ceil(time);
538
539                return time;
540        }
541        
542
543        public void renderInUICoords(ViewportAPI viewport) {
544        }
545
546        public void renderInWorldCoords(ViewportAPI viewport) {
547        }
548
549        public static float getFloatySize(ShipAPI ship) {
550                switch (ship.getHullSize()) {
551                case FIGHTER: return 15f;
552                case FRIGATE: return 17f;
553                case DESTROYER: return 21f;
554                case CRUISER: return 24f;
555                case CAPITAL_SHIP: return 27f;
556                }
557                return 10f;
558        }
559}