001package com.fs.starfarer.api.impl.combat; 002 003import java.awt.Color; 004import java.util.ArrayList; 005import java.util.List; 006 007import org.lwjgl.util.vector.Vector2f; 008 009import com.fs.starfarer.api.Global; 010import com.fs.starfarer.api.combat.BaseEveryFrameCombatPlugin; 011import com.fs.starfarer.api.combat.CollisionClass; 012import com.fs.starfarer.api.combat.CombatEngineAPI; 013import com.fs.starfarer.api.combat.EveryFrameCombatPlugin; 014import com.fs.starfarer.api.combat.MissileAPI; 015import com.fs.starfarer.api.combat.MutableShipStatsAPI; 016import com.fs.starfarer.api.combat.ShipAPI; 017import com.fs.starfarer.api.combat.ShipCommand; 018import com.fs.starfarer.api.combat.ShipSystemAPI; 019import com.fs.starfarer.api.combat.WeaponAPI; 020import com.fs.starfarer.api.combat.WeaponAPI.WeaponType; 021import com.fs.starfarer.api.input.InputEventAPI; 022import com.fs.starfarer.api.loading.WeaponSlotAPI; 023import com.fs.starfarer.api.util.Misc; 024 025/** 026 * The way to provide custom params is to have a derived class that sets p = <params> in its constructor. 027 * 028 * @author Alex 029 * 030 * Copyright 2022 Fractal Softworks, LLC 031 */ 032public class OrionDeviceStats extends BaseShipSystemScript { 033 034 public static class PusherPlateImpulse { 035 public float force; 036 public float dur; 037 public float elapsed; 038 } 039 public static class PusherPlateState { 040 public float compression; 041 public float vel; 042 public List<PusherPlateImpulse> impulses = new ArrayList<PusherPlateImpulse>(); 043 044 public void addImpulse(float force, float dur) { 045 PusherPlateImpulse ppi = new PusherPlateImpulse(); 046 ppi.force = force; 047 ppi.dur = dur; 048 impulses.add(ppi); 049 050 } 051 public void advance(float amount) { 052 List<PusherPlateImpulse> remove = new ArrayList<PusherPlateImpulse>(); 053 float totalForce = 0f; 054 for (PusherPlateImpulse curr : impulses) { 055 totalForce += curr.force; 056 curr.elapsed += amount; 057 if (curr.elapsed >= curr.dur) { 058 remove.add(curr); 059 } 060 } 061 impulses.removeAll(remove); 062 063 // assuming k of 1, and a mass of 1 064 float springForce = compression; 065 float netForce = totalForce - springForce; 066 067 vel += netForce * amount; 068 compression += vel * amount; 069 070 if (compression > 1f) { 071 compression = 1f; 072 vel = 0f; 073 } 074 float min = 0f; 075 //min = -0.5f; 076 if (compression < min) { 077 compression = min; 078 vel = 0f; 079 } 080 081 } 082 } 083 084 085 public static class OrionDeviceParams { 086 public float bombFadeInTime = 0.15f; 087 public float bombLiveTime = 0.25f; 088 public float bombSpeed = 50f; 089 public float bombInheritedVelocityFraction = 0.5f; 090 091 public float shapedExplosionOffset = 50f; 092 public float shapedExplosionEndSizeMin = 1f; 093 public float shapedExplosionEndSizeMax = 2f; 094 public Color shapedExplosionColor = new Color(255,125,25,155); 095 public int shapedExplosionNumParticles = 200; 096 public float shapedExplosionMinParticleSize = 80; 097 public float shapedExplosionMaxParticleSize = 100; 098 public float shapedExplosionScatter = 100f; 099 public float shapedExplosionMinParticleVel = 100; 100 public float shapedExplosionMaxParticleVel = 350f; 101 public float shapedExplosionMinParticleDur = 1f; 102 public float shapedExplosionMaxParticleDur = 2f; 103 public float shapedExplosionArc = 90f; 104 public Color jitterColor = new Color(255,125,25,55); 105 public float maxJitterDur = 2f; 106 107 public float pusherPlateMaxOffset = 14f; 108 public float pusherPlateImpulseForce = 10f; 109 public float pusherPlateImpulseDuration = 0.2f; 110 111 public float impactAccel = 5000f; 112 public float impactRateMult = 4f; 113 114 public boolean recolorTowardsEngineColor = false; 115 116 public String bombWeaponId = "od_bomblauncher"; 117 } 118 119 120 protected OrionDeviceParams p = new OrionDeviceParams(); 121 protected PusherPlateState pusherState = new PusherPlateState(); 122 123 public OrionDeviceStats() { 124 p = new OrionDeviceParams(); 125 //p.recolorTowardsEngineColor = true; 126 } 127 128 129 protected Color orig = null; 130 protected void recolor(ShipAPI ship) { 131 if (ship == null) return; 132 if (!p.recolorTowardsEngineColor) return; 133 134 if (orig == null) orig = p.shapedExplosionColor; 135 136 Color curr = ship.getEngineController().getFlameColorShifter().getCurr(); 137 138 p.shapedExplosionColor = Misc.interpolateColor(orig, curr, 0.75f); 139 p.shapedExplosionColor = Misc.setAlpha(p.shapedExplosionColor, orig.getAlpha()); 140 } 141 142 protected boolean wasIdle = false; 143 protected boolean deployedBomb = false; 144 public void apply(MutableShipStatsAPI stats, String id, State state, float effectLevel) { 145 ShipAPI ship = null; 146 //boolean player = false; 147 if (stats.getEntity() instanceof ShipAPI) { 148 ship = (ShipAPI) stats.getEntity(); 149 } else { 150 return; 151 } 152 153 recolor(ship); 154 155 if (effectLevel >= 1 && !deployedBomb) { 156 for (WeaponSlotAPI slot : ship.getHullSpec().getAllWeaponSlotsCopy()) { 157 if (slot.isSystemSlot()) { 158 spawnBomb(ship, slot); 159 } 160 } 161 deployedBomb = true; 162 //pusherPlateOffset = 1f; 163 } else if (state == State.COOLDOWN) { 164 deployedBomb = false; 165 } 166 167 168 float amount = Global.getCombatEngine().getElapsedInLastFrame(); 169 pusherState.advance(amount); 170 171 for (WeaponAPI w : ship.getAllWeapons()) { 172 Vector2f offset = new Vector2f(p.pusherPlateMaxOffset, 0f); 173 //pusherPlateOffset = 1f; 174 offset.scale(pusherState.compression); 175 if (w.getSpec().hasTag("pusherplate")) { 176 w.setRenderOffsetForDecorativeBeamWeaponsOnly(offset); 177 } 178 } 179 180 advanceImpl(amount, ship, state, effectLevel); 181 } 182 183 protected void advanceImpl(float amount, ShipAPI ship, State state, float effectLevel) { 184 185 } 186 187 188 public void unapply(MutableShipStatsAPI stats, String id) { 189 } 190 191 public void spawnBomb(ShipAPI source, WeaponSlotAPI slot) { 192 CombatEngineAPI engine = Global.getCombatEngine(); 193 Vector2f loc = slot.computePosition(source); 194 float angle = slot.computeMidArcAngle(source); 195 196 if (pusherState.compression > 0) { 197 Vector2f offset = new Vector2f(p.pusherPlateMaxOffset, 0f); 198 offset.scale(pusherState.compression); 199 offset = Misc.rotateAroundOrigin(offset, source.getFacing()); 200 Vector2f.add(loc, offset, loc); 201 } 202 203 MissileAPI bomb = (MissileAPI) engine.spawnProjectile(source, null, 204 p.bombWeaponId, 205 loc, 206 angle, source.getVelocity()); 207 if (source != null) { 208 Global.getCombatEngine().applyDamageModifiersToSpawnedProjectileWithNullWeapon( 209 source, WeaponType.MISSILE, false, bomb.getDamage()); 210 } 211 212 float fadeInTime = p.bombFadeInTime; 213 Vector2f inheritedVel = new Vector2f(source.getVelocity()); 214 inheritedVel.scale(p.bombInheritedVelocityFraction); 215 float speed = p.bombSpeed; 216 217 Vector2f vel = Misc.getUnitVectorAtDegreeAngle(angle); 218 vel.scale(speed); 219 Vector2f.add(vel, inheritedVel, vel); 220 bomb.getVelocity().set(vel); 221 bomb.fadeOutThenIn(fadeInTime); 222 223 bomb.setCollisionClass(CollisionClass.NONE); 224 bomb.setEmpResistance(1000); 225 bomb.setEccmChanceOverride(1f); 226 227 228 float liveTime = p.bombLiveTime; 229 bomb.setMaxFlightTime(liveTime); 230 231 232 Global.getCombatEngine().addPlugin(createBombImpactPlugin(source, slot, bomb, loc, angle)); 233 } 234 235 protected void notifySpawnedExplosionParticles(Vector2f bombLoc) { 236 237 } 238 239 protected EveryFrameCombatPlugin createBombImpactPlugin(final ShipAPI ship, final WeaponSlotAPI launchSlot, 240 final MissileAPI bomb, final Vector2f launchLoc, final float launchAngle) { 241 242 return new BaseEveryFrameCombatPlugin() { 243 float elapsed = 0f; 244 float impactTime = 0f; 245 float brakingTime = 0f; 246 float forceAngle; 247 float jitterTime = 0f; 248 boolean triggered = false; 249 boolean braking = false; 250 boolean done = false; 251 252 @Override 253 public void advance(float amount, List<InputEventAPI> events) { 254 if (Global.getCombatEngine().isPaused()) return; 255 256 elapsed += amount; 257 258 String impactCounterId = "od_system_counter"; 259 260 if (bomb.isFizzling()) { 261 if (!triggered) { 262 263 pusherState.addImpulse(p.pusherPlateImpulseForce, p.pusherPlateImpulseDuration); 264 265 forceAngle = Misc.getAngleInDegrees(bomb.getLocation(), launchSlot.computePosition(ship)); 266 float angleToShip = Misc.getAngleInDegrees(bomb.getLocation(), ship.getLocation()); 267 if (Misc.getAngleDiff(angleToShip, forceAngle) > 90f) { 268 forceAngle += 180f; 269 } 270 271 float diff = Misc.getAngleDiff(angleToShip, forceAngle); 272 float turnDir = Misc.getClosestTurnDirection(angleToShip, forceAngle); 273 forceAngle = angleToShip + diff * turnDir * 0.2f; 274 275 triggered = true; 276 ship.getMutableStats().getDynamic().getMod(impactCounterId).modifyFlat("od_launch_" + launchLoc, 1f); 277 278 if (Global.getCombatEngine().getViewport().isNearViewport(ship.getLocation(), 800f)) { 279 float angle = forceAngle + 180f; 280 281 int numParticles = p.shapedExplosionNumParticles; 282 float minSize = p.shapedExplosionMinParticleSize; 283 float maxSize = p.shapedExplosionMaxParticleSize; 284 Color pc = p.shapedExplosionColor; 285 286 float minDur = p.shapedExplosionMinParticleDur; 287 float maxDur = p.shapedExplosionMaxParticleDur; 288 289 float arc = p.shapedExplosionArc; 290 float scatter = p.shapedExplosionScatter; 291 float minVel = p.shapedExplosionMinParticleVel; 292 float maxVel = p.shapedExplosionMaxParticleVel; 293 294 float launchOffset = p.shapedExplosionOffset; 295 float endSizeMin = p.shapedExplosionEndSizeMin; 296 float endSizeMax = p.shapedExplosionEndSizeMax; 297 298 Vector2f spawnPoint = Misc.getUnitVectorAtDegreeAngle(forceAngle); 299 spawnPoint.scale(launchOffset); 300 Vector2f.add(bomb.getLocation(), spawnPoint, spawnPoint); 301 for (int i = 0; i < numParticles; i++) { 302 //p.setMaxAge(500 + (int)(Math.random() * 1000f)); 303 float angleOffset = (float) Math.random(); 304 if (angleOffset > 0.2f) { 305 angleOffset *= angleOffset; 306 } 307 float speedMult = 1f - angleOffset; 308 speedMult = 0.5f + speedMult * 0.5f; 309 angleOffset *= Math.signum((float) Math.random() - 0.5f); 310 angleOffset *= arc/2f; 311 float theta = (float) Math.toRadians(angle + angleOffset); 312 float r = (float) (Math.random() * Math.random() * scatter); 313 float x = (float)Math.cos(theta) * r; 314 float y = (float)Math.sin(theta) * r; 315 Vector2f pLoc = new Vector2f(spawnPoint.x + x, spawnPoint.y + y); 316 317 float speed = minVel + (maxVel - minVel) * (float) Math.random(); 318 speed *= speedMult; 319 320 Vector2f pVel = Misc.getUnitVectorAtDegreeAngle((float) Math.toDegrees(theta)); 321 pVel.scale(speed); 322 323 float pSize = minSize + (maxSize - minSize) * (float) Math.random(); 324 float pDur = minDur + (maxDur - minDur) * (float) Math.random(); 325 float endSize = endSizeMin + (endSizeMax - endSizeMin) * (float) Math.random(); 326 //Global.getCombatEngine().addSmoothParticle(pLoc, pVel, pSize, 1f, pDur, pc); 327 Global.getCombatEngine().addNebulaParticle(pLoc, pVel, pSize, endSize, 0.1f, 0.5f, pDur, pc); 328 //Global.getCombatEngine().addNebulaSmoothParticle(pLoc, pVel, pSize, endSize, 0.1f, 0.5f, pDur, pc); 329 //Global.getCombatEngine().addSwirlyNebulaParticle(pLoc, pVel, pSize, endSize, 0.1f, 0.5f, pDur, pc, false); 330 } 331 //Global.getCombatEngine().setPaused(true); 332 333 notifySpawnedExplosionParticles(bomb.getLocation()); 334 } 335 } 336 } 337 338 boolean multipleImpacts = ship.getMutableStats().getDynamic().getMod(impactCounterId).computeEffective(0) > 1; 339 340 String id = "od_system_mod"; 341 ship.getMutableStats().getMaxSpeed().unmodifyFlat(id); 342 //float maxSpeedWithoutBonus = ship.getMaxSpeedWithoutBoost(); 343 float maxSpeedWithoutBonus = ship.getMutableStats().getMaxSpeed().getModifiedValue(); 344 345// if (!triggered) { 346// ship.giveCommand(ShipCommand.ACCELERATE, null, 0); 347// } 348 349 if (triggered) { 350 jitterTime += amount; 351 float intensity = bomb.getFlightTime() / bomb.getMaxFlightTime(); 352 if (intensity > 1f) intensity = 1f; 353 if (triggered) intensity = 1f; 354 if (braking) { 355 intensity = 1f - brakingTime * 2f; 356 if (intensity < 0) intensity = 0; 357 } 358 float alt = 1f - (jitterTime / p.maxJitterDur); 359 if (alt < intensity) { 360 intensity = Math.max(alt, 0f); 361 } 362 Color jc = p.jitterColor; 363 ship.setJitter(this, jc, intensity, 3, 0f, 0f); 364 } 365 366 if (triggered && !braking) { 367 impactTime += amount * p.impactRateMult; 368 369 float mag = (1f - impactTime) * (1f - impactTime); 370 371 //mag = (float) Math.sin(impactTime * Math.PI); 372 //mag *= mag; 373 if (mag > 0) mag = (float) Math.sqrt(mag); 374 375 Vector2f forcePoint = launchSlot.computePosition(ship); 376 377 float dirToCenter = Misc.getAngleInDegrees(forcePoint, ship.getLocation()); 378 float angleDiff = Misc.getAngleDiff(forceAngle, dirToCenter); 379// if (angleDiff > 180f) { 380// angleDiff = 360f - angleDiff; 381// } 382 383 float totalAccel = p.impactAccel; 384 float portionAppliedToAngularVelocity = angleDiff * 1f / 90f; 385 if (portionAppliedToAngularVelocity > 1f) portionAppliedToAngularVelocity = 1f; 386 387 Vector2f acc = Misc.getUnitVectorAtDegreeAngle(forceAngle); 388 acc.scale(totalAccel * (1f - portionAppliedToAngularVelocity * 0.2f)); 389 acc.scale(mag * amount); 390 391 Vector2f.add(ship.getVelocity(), acc, ship.getVelocity()); 392 393 float angVelChange = portionAppliedToAngularVelocity * ship.getMaxTurnRate() * 0.25f; 394 angVelChange *= mag; 395 angVelChange *= Misc.getClosestTurnDirection(forceAngle, dirToCenter); 396 ship.setAngularVelocity(ship.getAngularVelocity() + angVelChange * amount); 397 398 float maxSpeedBoost = 1000f * Math.max(0f, (1f - portionAppliedToAngularVelocity) * 0.5f); 399 400 if (maxSpeedBoost > 0) { 401 ship.getMutableStats().getMaxSpeed().modifyFlat(id, maxSpeedBoost); 402 } else { 403 ship.getMutableStats().getMaxSpeed().unmodifyFlat(id); 404 } 405 ship.getMutableStats().getDeceleration().modifyFlat(id, 1f * Math.max(maxSpeedWithoutBonus, ship.getVelocity().length() - maxSpeedWithoutBonus)); 406 //ship.getMutableStats().getTurnAcceleration().modifyFlat(id, 100f * (1f - mag)); 407 //ship.giveCommand(ShipCommand.ACCELERATE, null, 0); 408 ship.blockCommandForOneFrame(ShipCommand.ACCELERATE); 409 ship.blockCommandForOneFrame(ShipCommand.ACCELERATE_BACKWARDS); 410 ship.giveCommand(ShipCommand.DECELERATE, null, 0); 411 //ship.getEngineController().forceShowAccelerating(); 412 if (impactTime >= 1f) { 413 braking = true; 414 //ship.getMutableStats().getTurnAcceleration().unmodify(id); 415 ship.getMutableStats().getMaxSpeed().unmodify(id); 416 } 417 } 418 419 if (braking) { 420 if (!multipleImpacts) { 421 ship.getMutableStats().getDeceleration().modifyFlat(id, 2f * Math.max(maxSpeedWithoutBonus, ship.getVelocity().length() - maxSpeedWithoutBonus)); 422 //ship.giveCommand(ShipCommand.ACCELERATE, null, 0); 423 ship.blockCommandForOneFrame(ShipCommand.ACCELERATE); 424 ship.blockCommandForOneFrame(ShipCommand.ACCELERATE_BACKWARDS); 425 ship.giveCommand(ShipCommand.DECELERATE, null, 0); 426 //ship.getEngineController().forceShowAccelerating(); 427 } 428 brakingTime += amount; 429 float threshold = 3f; 430 if (multipleImpacts) threshold = 0.1f; 431 if (brakingTime >= threshold || ship.getVelocity().length() <= maxSpeedWithoutBonus) { 432 done = true; 433 } 434 } 435 436 437 if ((!triggered && elapsed > 1f) || done) { 438 Global.getCombatEngine().removePlugin(this); 439 440 ship.getMutableStats().getDeceleration().unmodify(id); 441 //ship.getMutableStats().getTurnAcceleration().unmodify(id); 442 ship.getMutableStats().getMaxSpeed().unmodify(id); 443 444 ship.getMutableStats().getDynamic().getMod(impactCounterId).unmodifyFlat("od_launch_" + launchLoc); 445 } 446 } 447 }; 448 } 449 450 451 @Override 452 public boolean isUsable(ShipSystemAPI system, ShipAPI ship) { 453 if (ship.getEngineController().isFlamedOut() || ship.getEngineController().isFlamingOut()) { 454 return false; 455 } 456 return super.isUsable(system, ship); 457 } 458 459 460 461} 462 463 464 465 466 467 468 469