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}