001package com.fs.starfarer.api.impl.hullmods; 002 003import java.util.ArrayList; 004import java.util.Collections; 005import java.util.Comparator; 006import java.util.LinkedHashSet; 007import java.util.List; 008import java.util.Set; 009 010import java.awt.Color; 011 012import com.fs.starfarer.api.GameState; 013import com.fs.starfarer.api.Global; 014import com.fs.starfarer.api.combat.BaseHullMod; 015import com.fs.starfarer.api.combat.MutableShipStatsAPI; 016import com.fs.starfarer.api.combat.ShipAPI; 017import com.fs.starfarer.api.combat.ShipAPI.HullSize; 018import com.fs.starfarer.api.combat.WeaponAPI; 019import com.fs.starfarer.api.combat.WeaponAPI.WeaponSize; 020import com.fs.starfarer.api.combat.WeaponAPI.WeaponType; 021import com.fs.starfarer.api.impl.campaign.ids.Tags; 022import com.fs.starfarer.api.loading.WeaponSlotAPI; 023import com.fs.starfarer.api.ui.Alignment; 024import com.fs.starfarer.api.ui.TooltipMakerAPI; 025import com.fs.starfarer.api.util.IntervalUtil; 026import com.fs.starfarer.api.util.Misc; 027import com.fs.starfarer.api.util.TimeoutTracker; 028 029public class MissileAutoloader extends BaseHullMod { 030 031 public static class ReloadCapacityData { 032 public HullSize size; 033 public int minW, maxW; 034 public int capacity; 035 public ReloadCapacityData(HullSize size, int minW, int maxW, int capacity) { 036 this.size = size; 037 this.minW = minW; 038 this.maxW = maxW; 039 this.capacity = capacity; 040 } 041 042 public String getSizeStr() { 043 return Misc.getHullSizeStr(size); 044 } 045 046 public String getWeaponsString() { 047 if (maxW < 0) return "" + minW + "+"; 048 if (minW != maxW) return "" + minW + "-" + maxW; 049 return "" + minW; 050 } 051 } 052 053 public static List<ReloadCapacityData> CAPACITY_DATA = new ArrayList<MissileAutoloader.ReloadCapacityData>(); 054 static { 055 CAPACITY_DATA.add(new ReloadCapacityData(HullSize.FRIGATE, 1, 1, 6)); 056 CAPACITY_DATA.add(new ReloadCapacityData(HullSize.FRIGATE, 2, -1, 4)); 057 058 CAPACITY_DATA.add(new ReloadCapacityData(HullSize.DESTROYER, 1, 1, 9)); 059 CAPACITY_DATA.add(new ReloadCapacityData(HullSize.DESTROYER, 2, -1, 4)); 060 061 CAPACITY_DATA.add(new ReloadCapacityData(HullSize.CRUISER, 1, 2, 15)); 062 CAPACITY_DATA.add(new ReloadCapacityData(HullSize.CRUISER, 3, 3, 12)); 063 CAPACITY_DATA.add(new ReloadCapacityData(HullSize.CRUISER, 4, -1, 8)); 064 065 CAPACITY_DATA.add(new ReloadCapacityData(HullSize.CAPITAL_SHIP, 1, 3, 24)); 066 CAPACITY_DATA.add(new ReloadCapacityData(HullSize.CAPITAL_SHIP, 4, 6, 18)); 067 CAPACITY_DATA.add(new ReloadCapacityData(HullSize.CAPITAL_SHIP, 7, -1, 10)); 068 } 069 070 public static float BASIC_COOLDOWN = 5f; 071 public static float SMOD_COOLDOWN = 10f; 072 073 public static String MA_DATA_KEY = "core_missile_autoloader_data_key"; 074 075 public static class MissileAutoloaderData { 076 public IntervalUtil interval = new IntervalUtil(0.2f, 0.4f); 077 public float opLeft = 0f; 078 public float showExhaustedStatus = 5f; 079 public TimeoutTracker<WeaponAPI> cooldown = new TimeoutTracker<WeaponAPI>(); 080 } 081 082 public void applyEffectsBeforeShipCreation(HullSize hullSize, MutableShipStatsAPI stats, String id) { 083 } 084 085 086 @Override 087 public void advanceInCombat(ShipAPI ship, float amount) { 088 super.advanceInCombat(ship, amount); 089 090 if (!ship.isAlive()) return; 091 092 String key = MA_DATA_KEY; 093 ship.getCustomData().get(key); 094 MissileAutoloaderData data = (MissileAutoloaderData) ship.getCustomData().get(key); 095 if (data == null) { 096 data = new MissileAutoloaderData(); 097 ReloadCapacityData cap = getCapacityData(ship); 098 //data.opLeft = spec.getCostFor(ship.getHullSize()); 099 if (cap != null) { 100 data.opLeft = cap.capacity; 101 } else { 102 data.showExhaustedStatus = 0; 103 } 104 ship.setCustomData(key, data); 105 } 106 107 if (data.opLeft <= 0.05f) { 108 data.opLeft = 0f; 109 data.showExhaustedStatus -= amount; 110 if (data.showExhaustedStatus <= 0) { 111 return; 112 } 113 } 114 115 boolean playerShip = Global.getCurrentState() == GameState.COMBAT && 116 Global.getCombatEngine() != null && Global.getCombatEngine().getPlayerShip() == ship; 117 118 float mult = ship.getMutableStats().getMissileRoFMult().getModifiedValue(); 119 data.cooldown.advance(amount * mult); 120 for (WeaponAPI w : data.cooldown.getItems()) { 121 w.setRemainingCooldownTo(w.getCooldown()); 122 } 123 124 data.interval.advance(amount); 125 if (data.interval.intervalElapsed()) { 126 boolean playSound = false; 127 for (WeaponAPI w : ship.getAllWeapons()) { 128 if (!isAffected(w)) continue; 129 if (data.cooldown.contains(w)) continue; 130 131 if (w.usesAmmo() && w.getAmmo() <= 0) { 132 float reloadSize = w.getSpec().getMaxAmmo(); 133 float reloadCost = getReloadCost(w, ship); 134 float salvoSize = w.getSpec().getBurstSize(); 135 if (salvoSize < 1) salvoSize = 1; 136 if (reloadCost > data.opLeft) { 137 float f = data.opLeft / reloadCost; 138 if (f <= 0f) continue; 139 140 reloadSize *= f; 141 reloadSize /= salvoSize; 142 reloadSize = (float) Math.ceil(reloadSize); 143 reloadSize *= salvoSize; 144 reloadSize = (int) Math.round(reloadSize); 145 } 146 147 playSound = true; 148 149 w.setAmmo((int) reloadSize); 150 boolean sMod = isSMod(ship); 151 //if (COOLDOWN > 0) { 152 if (sMod) { 153 if (SMOD_COOLDOWN > 0) { 154 data.cooldown.set(w, SMOD_COOLDOWN); 155 } 156 } else { 157 if (BASIC_COOLDOWN > 0) { 158 data.cooldown.set(w, BASIC_COOLDOWN); 159 } 160 } 161 162 data.opLeft -= reloadCost; 163 //data.opLeft = Math.round(data.opLeft); 164 165 if (data.opLeft < 0) data.opLeft = 0; 166 if (data.opLeft <= 0) break; 167 } 168 } 169 170 playSound = false; // better without the sound I think 171 if (playerShip && playSound) { 172 Global.getSoundPlayer().playSound("missile_weapon_reloaded", 1f, 1f, ship.getLocation(), ship.getVelocity()); 173 } 174 } 175 176 if (playerShip) { 177 String status = "" + Misc.getRoundedValueOneAfterDecimalIfNotWhole(data.opLeft) + " CAPACITY REMAINING"; 178 if (data.opLeft <= 0) status = "CAPACITY EXHAUSTED"; 179 Global.getCombatEngine().maintainStatusForPlayerShip(data, 180 Global.getSettings().getSpriteName("ui", "icon_tactical_missile_autoloader"), 181 spec.getDisplayName(), 182 status, data.opLeft <= 0); 183 184 } 185 } 186 187 public static ReloadCapacityData getCapacityData(ShipAPI ship) { 188 if (ship == null) return null; 189 int count = 0; 190// for (WeaponAPI w : ship.getAllWeapons()) { 191// if (!isAffected(w)) continue; 192// if (w.getSlot().getSlotSize() != WeaponSize.SMALL || w.getSlot().getWeaponType() != WeaponType.MISSILE) { 193// count++; 194// } 195// } 196 for (WeaponSlotAPI slot : ship.getHullSpec().getAllWeaponSlotsCopy()) { 197 if (slot.getSlotSize() == WeaponSize.SMALL && 198 slot.getWeaponType() == WeaponType.MISSILE) { 199 count++; 200 } 201 } 202 203 for (ReloadCapacityData data : CAPACITY_DATA) { 204 if (data.size == ship.getHullSize()) { 205 if (count >= data.minW && count <= data.maxW) return data; 206 if (count >= data.minW && data.maxW < 0) return data; 207 } 208 } 209 return null; 210 } 211 212 public static boolean isAffected(WeaponAPI w) { 213 if (w == null) return false; 214 if (w.getType() != WeaponType.MISSILE) return false; 215 if (w.getSize() != WeaponSize.SMALL) return false; 216 217 if (w.getSlot().getWeaponType() != WeaponType.MISSILE) return false; 218 if (w.getSlot().getSlotSize() != WeaponSize.SMALL) return false; 219 220 if (w.getSpec().hasTag(Tags.NO_RELOAD)) return false; 221 if (!w.usesAmmo() || w.getAmmoPerSecond() > 0) return false; 222 if (w.isDecorative()) return false; 223 if (w.getSlot() != null && w.getSlot().isSystemSlot()) return false; 224 return true; 225 } 226 227 public static float getReloadCost(WeaponAPI w, ShipAPI ship) { 228 if (w.getSpec().hasTag(Tags.RELOAD_1PT)) return 1f; 229 if (w.getSpec().hasTag(Tags.RELOAD_1_AND_A_HALF_PT)) return 1.5f; 230 if (w.getSpec().hasTag(Tags.RELOAD_2PT)) return 2f; 231 if (w.getSpec().hasTag(Tags.RELOAD_3PT)) return 3f; 232 if (w.getSpec().hasTag(Tags.RELOAD_4PT)) return 4f; 233 if (w.getSpec().hasTag(Tags.RELOAD_5PT)) return 5f; 234 if (w.getSpec().hasTag(Tags.RELOAD_6PT)) return 6f; 235 236 int op = (int) Math.round(w.getSpec().getOrdnancePointCost(null, null)); 237 if (op == 1) return 1f; 238 if (op == 2 || op == 3) return 2f; 239 if (op == 4) return 3f; 240 if (op == 5 || op == 6) return 4f; 241 if (op == 7 || op == 8) return 5f; 242 return 6f; 243 } 244 245 @Override 246 public boolean shouldAddDescriptionToTooltip(HullSize hullSize, ShipAPI ship, boolean isForModSpec) { 247 return false; 248 } 249 250 @Override 251 public void addPostDescriptionSection(TooltipMakerAPI tooltip, HullSize hullSize, final ShipAPI ship, float width, boolean isForModSpec) { 252 float pad = 3f; 253 float opad = 10f; 254 Color h = Misc.getHighlightColor(); 255 Color bad = Misc.getNegativeHighlightColor(); 256 257 258 tooltip.addPara("A combat-rated autoloader that provides a limited number of reloads, out of a shared reload capacity, to " 259 + "missile weapons installed in small missile mounts.", opad, h, "missile weapons installed in small missile mounts"); 260// tooltip.addPara("Does not affect weapons that do not use ammo or already regenerate it, or weapons that are " 261// + "mounted in any other type of weapon slot." 262// + " The number of missiles reloaded is not affected by skills or hullmods that " 263// + "increase missile weapon ammo capacity.", opad); 264 tooltip.addPara("Does not affect weapons that do not use ammo or regenerate it, or are mounted in any other type of slot." 265 + " Reload size is not affected by skills or hullmods that " 266 + "increase missile ammo capacity.", opad); 267 268// tooltip.addPara("A combat-rated autoloader capable of reloading small missile weapons " 269// + "installed in small missile mounts" 270// + " a limited number of times. Does not affect weapons that do not use ammo or regenerate it." 271// + " The number of missiles reloaded is not affected by skills or hullmods that " 272// + "increase missile weapon ammo capacity.", opad, h, "small missile weapons installed in small missile mounts"); 273 274 tooltip.addSectionHeading("Reload capacity", Alignment.MID, opad); 275 tooltip.addPara("Determined by ship size and number of small missile " 276 + "slots, both filled and empty. " 277 + "Having fewer of these simplifies the task and " 278 + "increases the number of possible reloads.", opad); 279 280 if (isForModSpec || (ship == null && !Global.CODEX_TOOLTIP_MODE)) return; 281 282 tooltip.setBgAlpha(0.9f); 283 284 List<WeaponAPI> weapons = new ArrayList<WeaponAPI>(); 285 Set<String> seen = new LinkedHashSet<String>(); 286 if (ship != null) { 287 for (WeaponAPI w : ship.getAllWeapons()) { 288 if (!isAffected(w)) continue; 289 String id = w.getId(); 290 if (seen.contains(id)) continue; 291 seen.add(id); 292 weapons.add(w); 293 } 294 } 295 296 float numW = 130f; 297 float reloadW = 130f; 298 float sizeW = width - numW - reloadW - 10f; 299 tooltip.beginTable(Misc.getBasePlayerColor(), Misc.getDarkPlayerColor(), Misc.getBrightPlayerColor(), 300 20f, true, true, 301 new Object [] {"Ship size", sizeW, "Small missiles", numW, "Reload capacity", reloadW}); 302 303 ReloadCapacityData cap = getCapacityData(ship); 304 305 List<ReloadCapacityData> sortedCap = new ArrayList<ReloadCapacityData>(CAPACITY_DATA); 306 Collections.sort(sortedCap, new Comparator<ReloadCapacityData>() { 307 public int compare(ReloadCapacityData o1, ReloadCapacityData o2) { 308 //return (int) Math.signum(o1.capacity - o2.capacity); 309 if (o1.size != o2.size) { 310 return (int) Math.signum(o1.size.ordinal() - o2.size.ordinal()); 311 } 312 return (int) Math.signum(o1.capacity - o2.capacity); 313 } 314 }); 315 //sortedCap = new ArrayList<ReloadCapacityData>(CAPACITY_DATA); 316 317 HullSize prev = HullSize.FRIGATE; 318 for (ReloadCapacityData curr : sortedCap) { 319 Color c = Misc.getGrayColor(); 320 if (cap == curr || Global.CODEX_TOOLTIP_MODE) { 321 c = Misc.getHighlightColor(); 322 } 323 if (curr.size != hullSize && !Global.CODEX_TOOLTIP_MODE) continue; 324// if (prev != curr.size) { 325// tooltip.addRow("", "", ""); 326// } 327 tooltip.addRow(Alignment.MID, c, curr.getSizeStr(), 328 Alignment.MID, c, curr.getWeaponsString(), 329 Alignment.MID, c, "" + curr.capacity); 330 prev = curr.size; 331 } 332 tooltip.addTable("", 0, opad); 333 334 335 if (Global.CODEX_TOOLTIP_MODE) { 336 tooltip.addSpacer(5f); 337 return; 338 } 339 340 Collections.sort(weapons, new Comparator<WeaponAPI>() { 341 public int compare(WeaponAPI o1, WeaponAPI o2) { 342 float c1 = getReloadCost(o1, ship); 343 float c2 = getReloadCost(o2, ship); 344 return (int) Math.signum(c1 - c2); 345 } 346 }); 347 348 349 tooltip.addSectionHeading("Reload cost", Alignment.MID, opad + 5f); 350 351 float costW = 100f; 352 float nameW = width - costW - 5f; 353 tooltip.beginTable(Misc.getBasePlayerColor(), Misc.getDarkPlayerColor(), Misc.getBrightPlayerColor(), 354 20f, true, true, 355 new Object [] {"Affected weapon", nameW, "Reload cost", costW}); 356 int max = 10; 357 int count = 0; 358 for (WeaponAPI w : weapons) { 359 count++; 360 float cost = getReloadCost(w, ship); 361 String name = tooltip.shortenString(w.getDisplayName(), nameW - 20f); 362 tooltip.addRow(Alignment.LMID, Misc.getTextColor(), name, 363 Alignment.MID, h, Misc.getRoundedValueOneAfterDecimalIfNotWhole(cost)); 364 if (count >= max) break; 365 } 366 tooltip.addTable("No affected weapons mounted", weapons.size() - max, opad); 367 368 //tooltip.addPara("Weapons are reloaded, out of the shared pool of reload capacity, when they run out of ammo.", opad); 369 tooltip.addPara("A partial reload is possible when running out of capacity.", opad); 370// tooltip.addPara("A partial reload is possible when running out of capacity " 371// + "and the number of missiles loaded will be rounded up to the " 372// + "nearest multiple of the weapon's salvo size.", opad); 373 if (BASIC_COOLDOWN > 0) { 374 tooltip.addPara("After a reload, the weapon requires an extra %s seconds," 375 + " in addition to its normal cooldown, before it can fire again.", opad, 376 h, "" + (int) BASIC_COOLDOWN); 377 } 378 } 379 380 381 @Override 382 public boolean isApplicableToShip(ShipAPI ship) { 383 return getCapacityData(ship) != null; 384 } 385 386 387 @Override 388 public String getUnapplicableReason(ShipAPI ship) { 389 return "Ship does not have any small missile slots"; 390 } 391 392 @Override 393 public boolean isSModEffectAPenalty() { 394 return true; 395 } 396 397 @Override 398 public String getSModDescriptionParam(int index, HullSize hullSize, ShipAPI ship) { 399 if (index == 0) return "" + (int) SMOD_COOLDOWN; 400 return null; 401 } 402 403 404} 405 406 407 408 409 410 411 412 413 414 415