001package com.fs.starfarer.api.impl.campaign.terrain; 002 003import java.awt.Color; 004import java.util.ArrayList; 005import java.util.EnumSet; 006import java.util.HashMap; 007import java.util.Iterator; 008import java.util.LinkedHashMap; 009import java.util.List; 010import java.util.Map; 011 012import org.lwjgl.util.vector.Vector2f; 013 014import com.fs.starfarer.api.Global; 015import com.fs.starfarer.api.campaign.CampaignEngineLayers; 016import com.fs.starfarer.api.campaign.CampaignFleetAPI; 017import com.fs.starfarer.api.campaign.CampaignTerrainAPI; 018import com.fs.starfarer.api.campaign.LocationAPI; 019import com.fs.starfarer.api.campaign.SectorEntityToken; 020import com.fs.starfarer.api.combat.ViewportAPI; 021import com.fs.starfarer.api.ui.TooltipMakerAPI; 022import com.fs.starfarer.api.util.Misc; 023import com.fs.starfarer.api.util.TimeoutTracker; 024 025public class SlipstreamTerrainPlugin extends BaseTerrain { 026 027 public static final String LOCATION_SLIPSTREAM_KEY = "local_slipstream_terrain"; 028 029 public static final float MIN_POINT_DIST = 20f; 030 public static final float MAX_POINT_DIST = 250f; 031 032 public static final float WIDTH_GROWTH_PER_DAY = 200f; 033 034 035 public static CampaignTerrainAPI getSlipstream(LocationAPI location) { 036 if (location == null) return null; 037 return (CampaignTerrainAPI) location.getPersistentData().get(LOCATION_SLIPSTREAM_KEY); 038 } 039 040 public static SlipstreamTerrainPlugin getSlipstreamPlugin(LocationAPI location) { 041 CampaignTerrainAPI slipstream = getSlipstream(location); 042 if (slipstream != null) { 043 return (SlipstreamTerrainPlugin)slipstream.getPlugin(); 044 } 045 return null; 046 } 047 048 public static class StreamPoint { 049 public float x, y; 050 public float daysLeft; 051 public float burn; 052 public float width; 053 public StreamPoint(float x, float y, float daysLeft, float burn, float width) { 054 this.x = x; 055 this.y = y; 056 this.daysLeft = daysLeft; 057 this.burn = burn; 058 this.width = width; 059 } 060 } 061 062 public static class Stream { 063 transient public float minX, minY, maxX, maxY; 064 public List<StreamPoint> points = new ArrayList<StreamPoint>(); 065 //public List<StreamParticle> particles = new ArrayList<StreamParticle>(); 066 public SectorEntityToken key; 067 public int particlesPerPoint; 068 069 public Stream(SectorEntityToken key, int particlesPerPoint) { 070 this.key = key; 071 this.particlesPerPoint = particlesPerPoint; 072 readResolve(); 073 } 074 075 Object readResolve() { 076 minX = Float.MAX_VALUE; 077 minY = Float.MAX_VALUE; 078 maxX = -Float.MAX_VALUE; 079 maxY = -Float.MAX_VALUE; 080 for (StreamPoint p : points) { 081 updateMinMax(p); 082 } 083 return this; 084 } 085 086 public boolean isEmpty() { 087 return points.isEmpty(); 088 } 089 090 public StreamPoint getLastPoint() { 091 if (isEmpty()) return null; 092 return points.get(points.size() - 1); 093 } 094 095 public StreamPoint getFirstPoint() { 096 if (isEmpty()) return null; 097 return points.get(0); 098 } 099 100 private void updateMinMax(StreamPoint p) { 101 if (p.x < minX) minX = p.x; 102 if (p.y < minY) minY = p.y; 103 if (p.x > maxX) maxX = p.x; 104 if (p.y > maxY) maxY = p.y; 105 } 106 107 public void addPoint(StreamPoint p) { 108 updateMinMax(p); 109 points.add(p); 110 } 111 112 public boolean isNearViewport(ViewportAPI v, float pad) { 113 float x = v.getLLX(); 114 float y = v.getLLY(); 115 float w = v.getVisibleWidth(); 116 float h = v.getVisibleHeight(); 117 118 if (minX > x + w + pad) return false; 119 if (minY > y + h + pad) return false; 120 if (maxX < x - pad) return false; 121 if (maxY < y - pad) return false; 122 123 return true; 124 } 125 126 public boolean couldContainLocation(Vector2f loc, float radius) { 127 if (minX > loc.x + radius) return false; 128 if (minY > loc.y + radius) return false; 129 if (maxX < loc.x - radius) return false; 130 if (maxY < loc.y - radius) return false; 131 return true; 132 } 133 } 134 135 136 protected Map<SectorEntityToken, Stream> streams = new LinkedHashMap<SectorEntityToken, Stream>(); 137 transient protected Map<SectorEntityToken, StreamPoint> containsCache = new HashMap<SectorEntityToken, StreamPoint>(); 138 //transient protected SpriteAPI pointTexture; 139 140 protected TimeoutTracker<SectorEntityToken> disrupted = new TimeoutTracker<SectorEntityToken>(); 141 142 public void init(String terrainId, SectorEntityToken entity, Object param) { 143 super.init(terrainId, entity, param); 144 readResolve(); 145 } 146 147 public void advance(float amount) { 148 super.advance(amount); 149 if (true) return; 150 151 containsCache.clear(); 152 153 float days = Global.getSector().getClock().convertToDays(amount); 154 155 disrupted.advance(days); 156 157 for (CampaignFleetAPI fleet : entity.getContainingLocation().getFleets()) { 158 //if (!fleet.isPlayerFleet()) continue; 159 if (disrupted.contains(fleet)) continue; 160 161 float burnLevel = fleet.getCurrBurnLevel(); 162 if (burnLevel >= 1) { 163 Stream s = getStream(fleet); 164 Vector2f loc = fleet.getLocation(); 165 boolean addPoint = false; 166 if (s.isEmpty()) { 167 addPoint = true; 168 } else { 169 StreamPoint last = s.getLastPoint(); 170 float dist = Misc.getDistance(loc.x, loc.y, last.x, last.y); 171 if (dist > MIN_POINT_DIST) { 172 addPoint = true; 173 } 174 } 175 if (addPoint) { 176 StreamPoint p = new StreamPoint(loc.x, loc.y, 0.25f, 177 burnLevel, 178 Math.max(MIN_POINT_DIST * 2f, fleet.getRadius())); 179 s.addPoint(p); 180 } 181 } 182 } 183 184 Iterator<Stream> iter1 = streams.values().iterator(); 185 while (iter1.hasNext()) { 186 Stream s = iter1.next(); 187 188 advancePoints(s, days); 189 //advanceParticles(s, days); 190 //addNewParticles(s, days); 191 192 if (s.isEmpty()) {// && s.particles.isEmpty()) { 193 iter1.remove(); 194 } 195 } 196 } 197 198 public TimeoutTracker<SectorEntityToken> getDisrupted() { 199 return disrupted; 200 } 201 202 public void disrupt(CampaignFleetAPI fleet, float dur) { 203 disrupted.set(fleet, dur); 204 } 205 206 private void advancePoints(Stream s, float days) { 207 Iterator<StreamPoint> iter2 = s.points.iterator(); 208 while (iter2.hasNext()) { 209 StreamPoint p = iter2.next(); 210 p.daysLeft -= days; 211 p.width += WIDTH_GROWTH_PER_DAY * days; 212 if (p.daysLeft <= 0) { 213 iter2.remove(); 214 } 215 } 216 } 217 218 protected Stream getStream(SectorEntityToken key) { 219 Stream s = streams.get(key); 220 if (s == null) { 221 s = new Stream(key, 50); 222 streams.put(key, s); 223 } 224 return s; 225 } 226 227 @Override 228 public void applyEffect(SectorEntityToken entity, float days) { 229 if (entity instanceof CampaignFleetAPI) { 230 CampaignFleetAPI fleet = (CampaignFleetAPI) entity; 231 containsPointCaching(fleet, fleet.getLocation(), fleet.getRadius()); 232 if (containsCache.containsKey(fleet)) { 233 StreamPoint point = containsCache.get(fleet); 234 float fleetBurn = fleet.getFleetData().getBurnLevel(); 235 if (point.burn > fleetBurn || true) { 236 float diff = point.burn - fleetBurn; 237 if (diff > 2) diff = 2; 238 diff = (int) diff; 239 diff = 2; 240 fleet.getStats().addTemporaryModFlat(0.1f, getModId(), "In slipstream", diff, fleet.getStats().getFleetwideMaxBurnMod()); 241 } 242 } 243 } 244 } 245 246 @Override 247 public boolean containsEntity(SectorEntityToken other) { 248 return containsPointCaching(other, other.getLocation(), other.getRadius()) && !isPreventedFromAffecting(other); 249 } 250 251 @Override 252 public boolean containsPoint(Vector2f point, float radius) { 253 return containsPointCaching(null, point, radius); 254 } 255 256 private boolean containsPointCaching(SectorEntityToken key, Vector2f point, float radius) { 257 if (key != null && containsCache.containsKey(key)) { 258 return true; 259 } 260 if (true) return false; 261 262 float maxBurn = 0f; 263 StreamPoint result = null; 264 for (Stream s : streams.values()) { 265 if (s.key == key) continue; 266 267 if (!s.couldContainLocation(point, radius)) continue; 268 for (StreamPoint p : s.points) { 269// if (key instanceof CampaignFleetAPI) { 270// float fleetBurn = ((CampaignFleetAPI)key).getFleetData().getBurnLevel(); 271// if (p.burn <= fleetBurn) { 272// continue; 273// } 274// } 275 276 float dist = Misc.getDistance(p.x, p.y, point.x, point.y); 277 if (dist < p.width && maxBurn < p.burn) { 278 maxBurn = p.burn; 279 result = p; 280 } 281 } 282 } 283 284 if (result != null && key != null) { 285 containsCache.put(key, result); 286 } 287 288 return result != null; 289 } 290 291 292 293 Object readResolve() { 294 layers = EnumSet.of(CampaignEngineLayers.TERRAIN_7); 295 //pointTexture = Global.getSettings().getSprite("terrain", "slipstream_point"); 296 if (containsCache == null) { 297 containsCache = new HashMap<SectorEntityToken, StreamPoint>(); 298 } 299 return this; 300 } 301 302 Object writeReplace() { 303 return this; 304 } 305 306 transient private EnumSet<CampaignEngineLayers> layers = EnumSet.of(CampaignEngineLayers.TERRAIN_7); 307 public EnumSet<CampaignEngineLayers> getActiveLayers() { 308 return layers; 309 } 310 311 @Override 312 public boolean stacksWithSelf() { 313 return super.stacksWithSelf(); 314 } 315 316 @Override 317 public float getRenderRange() { 318 return Float.MAX_VALUE; 319 } 320 321 public boolean hasTooltip() { 322 return true; 323 } 324 325 public void createTooltip(TooltipMakerAPI tooltip, boolean expanded) { 326 float pad = 10f; 327 Color gray = Misc.getGrayColor(); 328 Color highlight = Misc.getHighlightColor(); 329 Color fuel = Global.getSettings().getColor("progressBarFuelColor"); 330 Color bad = Misc.getNegativeHighlightColor(); 331 332 tooltip.addTitle(getTerrainName()); 333 tooltip.addPara("Reduces the range at which fleets inside it can be detected by %s.", pad, 334 highlight, 335 "50%" 336 ); 337 tooltip.addPara("Does not stack with other similar terrain effects.", pad); 338 } 339 340 public boolean isTooltipExpandable() { 341 return false; 342 } 343 344 public float getTooltipWidth() { 345 return 350f; 346 } 347 348 public String getTerrainName() { 349 return "Slipstream"; 350 } 351 352 public String getNameForTooltip() { 353 return "Slipstream"; 354 } 355 356 public String getEffectCategory() { 357 return "slipstream"; 358 } 359} 360 361 362 363//public static class StreamParticle { 364// public float x, y; 365// public float age, maxAge; 366// public StreamPoint point; 367// //public Vector2f lastVel = null; 368// //public LinkedList<Vector2f> prevLocs = new LinkedList<Vector2f>(); 369// public Color color; 370//} 371 372//private void advanceParticles(Stream s, float days) { 373// Iterator<StreamParticle> iter3 = s.particles.iterator(); 374// while (iter3.hasNext()) { 375// StreamParticle p = iter3.next(); 376// p.age += days; 377// if (p.age >= p.maxAge) { 378// iter3.remove(); 379// } else if (p.point != null) { 380// StreamPoint curr = p.point; 381// StreamPoint prev = null, next = null; 382// int index = s.points.indexOf(curr); 383// if (index != -1) { 384// if (index > 0) { 385// prev = s.points.get(index - 1); 386// } 387// if (index < s.points.size() - 1) { 388// next = s.points.get(index + 1); 389// } 390// } 391// Vector2f vel = new Vector2f(); 392// Vector2f cv = new Vector2f(curr.x, curr.y); 393// Vector2f pLoc = new Vector2f(p.x, p.y); 394// float maxSpeed = 1000f; 395// float minSpeed = 1000f; 396// float maxDist = 200f; 397// Vector2f unitDirPrev = null; 398// if (prev != null) { 399// Vector2f pv = new Vector2f(prev.x, prev.y); 400// Vector2f dir = Vector2f.sub(cv, pv, new Vector2f()); 401// unitDirPrev = new Vector2f(dir); 402// Misc.normalise(dir); 403// float dist = Misc.getDistance(pLoc, pv); 404// float speed = minSpeed; 405// if (dist < maxDist) { 406// speed = minSpeed + (maxSpeed - minSpeed) * (1f - dist / maxDist); 407// } 408// dir.scale(speed); 409// Vector2f.add(vel, dir, vel); 410// } 411// if (next != null) { 412// Vector2f nv = new Vector2f(next.x, next.y); 413// Vector2f dir = Vector2f.sub(nv, cv, new Vector2f()); 414// Misc.normalise(dir); 415// float dist = Misc.getDistance(cv, pLoc); 416// boolean angleOk = unitDirPrev == null || Vector2f.dot(unitDirPrev, dir) > 0; 417// if (dist < MAX_POINT_DIST && angleOk) { 418// float speed = minSpeed; 419// if (dist < maxDist) { 420// speed = minSpeed + (maxSpeed - minSpeed) * (1f - dist / maxDist); 421// } 422// dir.scale(speed); 423// Vector2f.add(vel, dir, vel); 424// 425// float distNext = Misc.getDistance(nv, pLoc); 426// if (distNext < dist) { 427// p.point = next; 428// } 429// } 430// } 431// 432// p.x += vel.x * days; 433// p.y += vel.y * days; 434// 435// pLoc.set(p.x, p.y); 436// if (p.prevLocs.size() < 2) { 437// p.prevLocs.add(pLoc); 438// } else { 439// float dist = Misc.getDistance(p.prevLocs.get(p.prevLocs.size() - 2), p.prevLocs.getLast()); 440// if (dist >= 2) { 441// p.prevLocs.add(pLoc); 442// } else { 443// p.prevLocs.getLast().set(pLoc); 444// } 445// } 446// 447// 448// while (p.prevLocs.size() > 5) { 449// p.prevLocs.removeFirst(); 450// } 451// } 452// } 453//} 454// 455// 456//private void addNewParticles(Stream s, float days) { 457// if (s.isEmpty()) return; 458// 459// if (!s.key.isInCurrentLocation() || !s.isNearViewport(Global.getSector().getViewport(), 500f)) { 460// return; 461// } 462// 463// int points = s.points.size(); 464// int maxParticles = (points - 0) * s.particlesPerPoint; 465// 466// WeightedRandomPicker<Color> colorPicker = new WeightedRandomPicker<Color>(); 467// if (s.key instanceof CampaignFleetAPI) { 468// CampaignFleetAPI fleet = (CampaignFleetAPI) s.key; 469// for (FleetMemberViewAPI view : fleet.getViews()) { 470// colorPicker.add(view.getContrailColor().getCurr(), view.getMember().getHullSpec().getHullSize().ordinal()); 471// } 472// } 473// if (colorPicker.isEmpty()) { 474// colorPicker.add(new Color(100, 150, 255, 255), 1f); 475// } 476// 477// Random r = new Random(); 478// for (int i = 0; i < s.particlesPerPoint * 0.1f; i++) { 479// if (s.particles.size() >= maxParticles || s.isEmpty() || s.points.size() <= 1) { 480// break; 481// } 482// 483// int index = r.nextInt(s.points.size() - 1); 484// StreamPoint p = s.points.get(index); 485// if (index < s.points.size() - 1) { 486// StreamPoint next = s.points.get(index + 1); 487// float dist = Misc.getDistance(p.x, p.y, next.x, next.y); 488// if (dist > MAX_POINT_DIST) continue; 489// } 490// 491// StreamParticle particle = new StreamParticle(); 492// float spread = 10f + (s.points.size() - index) * 10f; 493// particle.x = p.x + (float) Math.random() * spread - spread/2f; 494// particle.y = p.y + (float) Math.random() * spread - spread/2f; 495//// particle.x = p.x; 496//// particle.y = p.y; 497// particle.age = 0; 498// particle.maxAge = 0.1f + 0.1f * (float) Math.random(); 499// particle.point = p; 500// 501// particle.color = colorPicker.pick(); 502// 503// s.particles.add(particle); 504// } 505//} 506//public void render(CampaignEngineLayers layer, ViewportAPI viewport) { 507// super.render(layer, viewport); 508// 509// float alphaMult = viewport.getAlphaMult(); 510// 511// for (Stream s : streams.values()) { 512// if (!s.isNearViewport(viewport, 300f)) continue; 513// if (s.particles.isEmpty()) continue; 514// GL11.glEnable(GL11.GL_BLEND); 515// GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE); 516// //GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); 517// //Color color = new Color(100, 150, 255, 255); 518//// GL11.glColor4ub((byte)color.getRed(), 519//// (byte)color.getGreen(), 520//// (byte)color.getBlue(), 521//// (byte)((float)color.getAlpha() * alphaMult)); 522// 523// pointTexture.bindTexture(); 524// GL11.glBegin(GL11.GL_QUADS); 525// StreamPoint last = null; 526// if (!s.points.isEmpty()) { 527// last = s.points.get(s.points.size() - 1); 528// } 529// for (StreamParticle p : s.particles) { 530//// if (p.prevLocs == null || p.prevLocs.size() < 5) continue; 531//// float index = 0f; 532//// float max = p.prevLocs.size(); 533//// for (Vector2f loc : p.prevLocs) { 534//// float b; 535//// float t = Math.min(0.05f, p.maxAge / 2f); 536//// if (p.age < t) { 537//// b = p.age / t; 538//// } else { 539//// b = 1f - (p.age - t) / (p.maxAge - t); 540//// } 541//// b *= 1f - ((index + 1) / max); 542//// if (index == 0) b = 0; 543//// if (index == max - 1) b = 0; 544//// 545//// GL11.glColor4ub((byte)color.getRed(), 546//// (byte)color.getGreen(), 547//// (byte)color.getBlue(), 548//// (byte)((float)color.getAlpha() * alphaMult * b)); 549//// 550//// float size = 7f; 551//// GL11.glTexCoord2f(0, 0); 552//// GL11.glVertex2f(loc.x - size/2f, loc.y - size/2f); 553//// 554//// GL11.glTexCoord2f(0.01f, 0.99f); 555//// GL11.glVertex2f(loc.x - size/2f, loc.y + size/2f); 556//// 557//// GL11.glTexCoord2f(0.99f, 0.99f); 558//// GL11.glVertex2f(loc.x + size/2f, loc.y + size/2f); 559//// 560//// GL11.glTexCoord2f(0.99f, 0.01f); 561//// GL11.glVertex2f(loc.x + size/2f, loc.y - size/2f); 562//// 563//// index++; 564//// } 565// 566// float b; 567// float t = Math.min(0.05f, p.maxAge / 2f); 568// if (p.age < t) { 569// b = p.age / t; 570// } else { 571// b = 1f - (p.age - t) / (p.maxAge - t); 572// } 573// b *= 4f; 574// 575// if (last != null) { 576//// if (s.key.isPlayerFleet()) { 577//// System.out.println("23rasdf"); 578//// } 579// float dist = Misc.getDistance(last.x, last.y, p.x, p.y); 580// float max = s.key.getRadius() + 50f; 581// if (dist < max) { 582// b *= Math.max(0, (dist - s.key.getRadius())/ (max - s.key.getRadius())); 583// if (b <= 0) { 584// p.age = p.maxAge + 1f; 585// } 586// } 587// } 588// if (p.age >= p.maxAge) { 589// b = 0f; 590// } 591// 592// Color color = p.color; 593// float alpha = ((float)color.getAlpha() * alphaMult * b); 594// if (alpha > 255f) alpha = 255f; 595// GL11.glColor4ub((byte)color.getRed(), 596// (byte)color.getGreen(), 597// (byte)color.getBlue(), 598// (byte)(alpha)); 599// 600// float size = 7f; 601// GL11.glTexCoord2f(0, 0); 602// GL11.glVertex2f(p.x - size/2f, p.y - size/2f); 603// 604// GL11.glTexCoord2f(0.01f, 0.99f); 605// GL11.glVertex2f(p.x - size/2f, p.y + size/2f); 606// 607// GL11.glTexCoord2f(0.99f, 0.99f); 608// GL11.glVertex2f(p.x + size/2f, p.y + size/2f); 609// 610// GL11.glTexCoord2f(0.99f, 0.01f); 611// GL11.glVertex2f(p.x + size/2f, p.y - size/2f); 612// } 613// 614// GL11.glEnd(); 615// 616// } 617//}