001package com.fs.starfarer.api.characters; 002 003import java.util.ArrayList; 004import java.util.Collections; 005import java.util.LinkedHashMap; 006import java.util.LinkedHashSet; 007import java.util.List; 008import java.util.Map; 009import java.util.Set; 010 011import com.fs.starfarer.api.Global; 012import com.fs.starfarer.api.characters.MutableCharacterStatsAPI.SkillLevelAPI; 013import com.fs.starfarer.api.fleet.FleetMemberAPI; 014import com.fs.starfarer.api.impl.campaign.ids.MemFlags; 015import com.fs.starfarer.api.impl.campaign.ids.Stats; 016import com.fs.starfarer.api.impl.campaign.plog.OfficerSkillGainRecord; 017import com.fs.starfarer.api.impl.campaign.plog.PlaythroughLog; 018import com.fs.starfarer.api.ui.Alignment; 019import com.fs.starfarer.api.ui.ButtonAPI; 020import com.fs.starfarer.api.ui.TooltipMakerAPI; 021import com.fs.starfarer.api.ui.TooltipMakerAPI.TooltipCreator; 022import com.fs.starfarer.api.ui.TooltipMakerAPI.TooltipLocation; 023import com.fs.starfarer.api.util.Misc; 024 025public class SkillsChangeOfficerEffect extends BaseSkillsChangeEffect { 026 027 public static class OfficerEffectData { 028 OfficerDataAPI data; 029 int newLevel; 030 List<String> removeSkills = new ArrayList<String>(); 031 List<String> removeElite = new ArrayList<String>(); 032 FleetMemberAPI member; 033 boolean unusable = false; 034 boolean makeMercenary; 035 036 ButtonAPI buttonMerc; 037 ButtonAPI buttonOther; 038 039 public boolean hasChanges() { 040 return data.getPerson().getStats().getLevel() != newLevel || unusable || 041 !removeSkills.isEmpty() || !removeElite.isEmpty() || makeMercenary; 042 } 043 } 044 045 public static class OfficerDataMap { 046 Map<PersonAPI, OfficerEffectData> map = new LinkedHashMap<PersonAPI, SkillsChangeOfficerEffect.OfficerEffectData>(); 047 } 048 public void setMap(OfficerDataMap map, Map<String, Object> dataMap) { 049 String key = getClass().getSimpleName(); 050 dataMap.put(key, map); 051 } 052 public OfficerDataMap getMap(Map<String, Object> dataMap) { 053 String key = getClass().getSimpleName(); 054 OfficerDataMap map = (OfficerDataMap)dataMap.get(key); 055 if (map == null) { 056 map = new OfficerDataMap(); 057 dataMap.put(key, map); 058 } 059 return map; 060 } 061 062 063 public int getMaxLevel(MutableCharacterStatsAPI stats) { 064 int bonus = (int) stats.getDynamic().getMod(Stats.OFFICER_MAX_LEVEL_MOD).computeEffective(0); 065 return (int) Global.getSettings().getFloat("officerMaxLevel") + bonus; 066 } 067 068 public int getMaxEliteSkills(MutableCharacterStatsAPI stats) { 069 int bonus = (int) stats.getDynamic().getMod(Stats.OFFICER_MAX_ELITE_SKILLS_MOD).computeEffective(0); 070 return (int) Global.getSettings().getFloat("officerMaxEliteSkills") + bonus; 071 } 072 073 public int getNumEliteSkills(PersonAPI person) { 074 int num = 0; 075 for (SkillLevelAPI sl : person.getStats().getSkillsCopy()) { 076 if (sl.getLevel() >= 2f) { 077 num++; 078 } 079 } 080 return num; 081 } 082 083 public OfficerDataMap getEffects(MutableCharacterStatsAPI from, MutableCharacterStatsAPI to) { 084 OfficerDataMap result = new OfficerDataMap(); 085 086 087 int maxOfficersPre = (int) from.getOfficerNumber().getModifiedValue(); 088 int maxOfficersPost = (int) to.getOfficerNumber().getModifiedValue(); 089 090 Map<OfficerDataAPI, FleetMemberAPI> members = new LinkedHashMap<OfficerDataAPI, FleetMemberAPI>(); 091 Set<OfficerDataAPI> unusable = new LinkedHashSet<OfficerDataAPI>(); 092 if (maxOfficersPre > maxOfficersPost) { 093 int count = 0; 094 for (OfficerDataAPI officer : Global.getSector().getPlayerFleet().getFleetData().getOfficersCopy()) { 095 boolean merc = Misc.isMercenary(officer.getPerson()); 096 if (!merc) { 097 count++; 098 } 099 if (count > maxOfficersPost && !merc) { 100 unusable.add(officer); 101 } 102 } 103 } 104 105 for (OfficerDataAPI officer : Global.getSector().getPlayerFleet().getFleetData().getOfficersCopy()) { 106 for (FleetMemberAPI member : Global.getSector().getPlayerFleet().getFleetData().getMembersListCopy()) { 107 if (member.getCaptain() == officer.getPerson()) { 108 members.put(officer, member); 109 break; 110 } 111 } 112 } 113 114 int maxLevelPre = getMaxLevel(from); 115 int maxLevelPost = getMaxLevel(to); 116 117 int maxElitePre = getMaxEliteSkills(from); 118 int maxElitePost = getMaxEliteSkills(to); 119 120 for (OfficerDataAPI data : Global.getSector().getPlayerFleet().getFleetData().getOfficersCopy()) { 121 PersonAPI person = data.getPerson(); 122 MutableCharacterStatsAPI stats = person.getStats(); 123 124 if (Misc.isMercenary(person)) { 125 continue; 126 } 127 128 boolean pods = person.getMemoryWithoutUpdate().getBoolean(MemFlags.EXCEPTIONAL_SLEEPER_POD_OFFICER); 129 if (pods) continue; 130 131 OfficerEffectData effect = new OfficerEffectData(); 132 effect.data = data; 133 effect.newLevel = stats.getLevel(); 134 135 if (members.containsKey(data)) { 136 effect.member = members.get(data); 137 } 138 if (unusable.contains(data)) { 139 effect.unusable = true; 140 } 141 142 if (stats.getLevel() > maxLevelPost && maxLevelPost < maxLevelPre) { 143 final List<String> skillsInOrderOfGain = new ArrayList<String>(); 144 for (OfficerSkillGainRecord rec : PlaythroughLog.getInstance().getOfficerSkillsLearned()) { 145 if (rec.getPersonId().equals(person.getId()) && !rec.isElite() && 146 stats.getSkillLevel(rec.getSkillId()) > 0) { 147 skillsInOrderOfGain.add(rec.getSkillId()); 148 } 149 } 150 Collections.reverse(skillsInOrderOfGain); 151 152 // if the skill records aren't present, assume it's a cryopods officer 153 // it can also be an officer from 0.95a before officer skill gain was tracked 154 // so, erring on the side of not nuking skills needlessly 155 int skillsToRemove = stats.getLevel() - maxLevelPost; 156 for (int i = 0; i < skillsToRemove && !skillsInOrderOfGain.isEmpty(); i++) { 157 effect.removeSkills.add(skillsInOrderOfGain.remove(0)); 158 } 159 if (!effect.removeSkills.isEmpty()) { 160 effect.newLevel = maxLevelPost; 161 } 162 } 163 164 int numElite = getNumEliteSkills(person); 165 if (numElite > maxElitePost && maxElitePost < maxElitePre) { 166 final List<String> eliteInOrderOfGain = new ArrayList<String>(); 167 for (OfficerSkillGainRecord rec : PlaythroughLog.getInstance().getOfficerSkillsLearned()) { 168 if (rec.getPersonId().equals(person.getId()) && rec.isElite() && 169 stats.getSkillLevel(rec.getSkillId()) >= 2 && 170 !effect.removeSkills.contains(rec.getSkillId())) { 171 eliteInOrderOfGain.add(rec.getSkillId()); 172 } 173 } 174 Collections.reverse(eliteInOrderOfGain); 175 176 int eliteToRemove = numElite - maxElitePost; 177 for (int i = 0; i < eliteToRemove && !eliteInOrderOfGain.isEmpty(); i++) { 178 effect.removeElite.add(eliteInOrderOfGain.remove(0)); 179 } 180 } 181 182 183 if (effect.hasChanges()) { 184 result.map.put(person, effect); 185 } 186 } 187 return result; 188 } 189 190 191 @Override 192 public boolean hasEffects(MutableCharacterStatsAPI from, MutableCharacterStatsAPI to) { 193 return !getEffects(from, to).map.isEmpty(); 194 } 195 196 @Override 197 public void printEffects(MutableCharacterStatsAPI from, MutableCharacterStatsAPI to, TooltipMakerAPI info, Map<String, Object> dataMap) { 198 super.prepare(); 199 200 OfficerDataMap map = getEffects(from, to); 201 setMap(map, dataMap); 202 203 float bw = 470; 204 float bh = 25; 205 float pad = 3f; 206 float opad = 10f; 207 208 209 info.addSectionHeading("Officers", base, dark, Alignment.MID, 15f); 210 211 info.addPara("Officers whose skills that exceed the new limits will have their excess skills removed, or become mercenaries on a temporary contract. " 212 + "Officers that already had those kinds of skills when acquired will not be affected.", opad, 213 Misc.getNegativeHighlightColor(), 214 "excess skills removed"); 215 float initPad = opad; 216 for (final OfficerEffectData data : map.map.values()) { 217 final PersonAPI person = data.data.getPerson(); 218 String str = person.getRank() + " " + person.getNameString(); 219 if (data.member == null) { 220 str += ", unassigned, will..."; 221 } else { 222 str += ", commanding the " + data.member.getShipName() + 223 " (" + data.member.getHullSpec().getHullNameWithDashClass() + "), will..."; 224 } 225 226 String mercText = "Become a mercenary, drawing higher pay on a new contract"; 227 String otherText = ""; 228 229 if (data.unusable && data.removeElite.isEmpty() && data.removeSkills.isEmpty()) { 230 if (data.member == null) { 231 otherText = "Become unable to be assigned command of a ship"; 232 } else { 233 otherText = "Be relieved of command, and unable to be assigned command of a ship"; 234 } 235 } else { 236 String part1 = ""; 237 if (data.unusable) { 238 part1 = "Be relieved of command, and"; 239 } 240 String part2 = ""; 241 if (!data.removeElite.isEmpty() && data.removeSkills.isEmpty()) { 242 part2 = " lose excess elite skill effects"; 243 } else if (data.removeElite.isEmpty() && !data.removeSkills.isEmpty()) { 244 part2 = " lose excess skills"; 245 } else { 246 part2 = " lose excess skills and elite skill effects"; 247 } 248 if (part1.isEmpty()) { 249 part2 = part2.trim(); 250 otherText = Misc.ucFirst(part2); 251 } else { 252 otherText = part1 + part2; 253 } 254 } 255 256 info.addPara(str, opad + 5f); 257 258 float indent = 40f; 259 data.buttonOther = info.addAreaCheckbox(otherText, new Object(), base, dark, bright, bw, bh, opad, true); 260 data.buttonOther.getPosition().setXAlignOffset(indent); 261 262 info.addTooltipToPrevious(new TooltipCreator() { 263 public boolean isTooltipExpandable(Object tooltipParam) { 264 return false; 265 } 266 public float getTooltipWidth(Object tooltipParam) { 267 return 450; 268 } 269 public void createTooltip(TooltipMakerAPI tooltip, boolean expanded, Object tooltipParam) { 270 float opad = 10f; 271 float pad = 3f; 272 tooltip.addPara("This officer has the following skills:", 0f); 273 tooltip.addSkillPanel(person, pad); 274 275 if (!data.removeSkills.isEmpty()) { 276 tooltip.addPara("They will lose the following skills:", opad); 277 PersonAPI fake = Global.getFactory().createPerson(); 278 for (String skillId : data.removeSkills) { 279 fake.getStats().setSkillLevel(skillId, person.getStats().getSkillLevel(skillId)); 280 } 281 tooltip.addSkillPanel(fake, pad); 282 } 283 if (!data.removeElite.isEmpty()) { 284 tooltip.addPara("They will lose the elite effects of the following skills:", opad); 285 PersonAPI fake = Global.getFactory().createPerson(); 286 for (String skillId : data.removeElite) { 287 fake.getStats().setSkillLevel(skillId, person.getStats().getSkillLevel(skillId)); 288 } 289 tooltip.addSkillPanel(fake, pad); 290 } 291 } 292 }, TooltipLocation.RIGHT); 293 294 //data.buttonMerc = info.addAreaCheckbox(mercText, new Object(), base, dark, bright, bw, bh, pad, true); 295 data.buttonMerc = info.addAreaCheckbox(mercText, new Object(), sBase, sDark, sBright, bw, bh, pad, true); 296 data.buttonOther.setChecked(true); 297 data.buttonMerc.setChecked(false); 298 299 info.addTooltipToPrevious(new TooltipCreator() { 300 public boolean isTooltipExpandable(Object tooltipParam) { 301 return false; 302 } 303 public float getTooltipWidth(Object tooltipParam) { 304 return 450; 305 } 306 public void createTooltip(TooltipMakerAPI tooltip, boolean expanded, Object tooltipParam) { 307 int payPre = (int) Misc.getOfficerSalary(person, false); 308 int payPost = (int) Misc.getOfficerSalary(person, true); 309 int contractDur = (int) Global.getSettings().getFloat("officerMercContractDur"); 310 tooltip.addPara("This officer's pay will be increased from %s to %s per month," 311 + " and they will want to leave after %s days, when their contract expires.", 0f, 312 Misc.getHighlightColor(), 313 Misc.getDGSCredits(payPre), 314 Misc.getDGSCredits(payPost), 315 "" + contractDur 316 ); 317 tooltip.addPara("Extending the contract beyond that will require a %s.", 10f, 318 Misc.getStoryOptionColor(), Misc.STORY + " point"); 319 } 320 }, TooltipLocation.RIGHT); 321 322 info.addSpacer(0).getPosition().setXAlignOffset(-indent); 323 } 324 } 325 326 @Override 327 public void infoButtonPressed(ButtonAPI button, Object param, Map<String, Object> dataMap) { 328 OfficerDataMap map = getMap(dataMap); 329 330 for (OfficerEffectData data : map.map.values()) { 331 if (data.buttonMerc == button) { 332 data.makeMercenary = true; 333 data.buttonMerc.setChecked(true); 334 data.buttonOther.setChecked(false); 335 //System.out.println("Merc button, " + data.data.getPerson().getNameString()); 336 } else if (data.buttonOther == button) { 337 data.makeMercenary = false; 338 data.buttonMerc.setChecked(false); 339 data.buttonOther.setChecked(true); 340 //System.out.println("Other button, " + data.data.getPerson().getNameString()); 341 } 342 } 343 } 344 345 @Override 346 public void applyEffects(MutableCharacterStatsAPI from, MutableCharacterStatsAPI to, Map<String, Object> dataMap) { 347 OfficerDataMap map = getMap(dataMap); 348 349 for (OfficerEffectData data : map.map.values()) { 350 if (data.makeMercenary) { 351 Misc.setMercenary(data.data.getPerson(), true); 352 Misc.setMercHiredNow(data.data.getPerson()); 353 } else { 354 if (data.unusable && data.member != null) { 355 data.member.setCaptain(Global.getFactory().createPerson()); 356 } 357 MutableCharacterStatsAPI stats = data.data.getPerson().getStats(); 358 for (String id : data.removeElite) { 359 stats.setSkillLevel(id, 1); 360 PlaythroughLog.getInstance().removeOfficerSkillRecord(data.data.getPerson().getId(), id, true); 361 } 362 for (String id : data.removeSkills) { 363 stats.setSkillLevel(id, 0); 364 PlaythroughLog.getInstance().removeOfficerSkillRecord(data.data.getPerson().getId(), id, false); 365 PlaythroughLog.getInstance().removeOfficerSkillRecord(data.data.getPerson().getId(), id, true); 366 } 367 if (data.newLevel != stats.getLevel()) { 368 stats.setLevel(data.newLevel); 369 } 370 } 371 372 } 373 } 374 375 376} 377 378 379