/* * Copyright (C) 2016 MegaMek team * * This file is part of MekHQ. * * MekHQ is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * MekHQ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with MekHQ. If not, see <http://www.gnu.org/licenses/>. */ package mekhq.campaign.mod.am; import java.util.Arrays; import java.util.Collection; import java.util.EnumSet; import java.util.List; import megamek.common.Compute; import mekhq.MekHQ; import mekhq.Utilities; import mekhq.campaign.Campaign; import mekhq.campaign.GameEffect; import mekhq.campaign.LogEntry; import mekhq.campaign.personnel.BodyLocation; import mekhq.campaign.personnel.Injury; import mekhq.campaign.personnel.InjuryLevel; import mekhq.campaign.personnel.InjuryType; import mekhq.campaign.personnel.Modifier; import mekhq.campaign.personnel.Person; /** Advanced Medical sub-system injury types */ public final class InjuryTypes { // Predefined types public static final InjuryType CUT = new InjuryTypes.Cut(); public static final InjuryType BRUISE = new InjuryTypes.Bruise(); public static final InjuryType LACERATION = new InjuryTypes.Laceration(); public static final InjuryType SPRAIN = new InjuryTypes.Sprain(); public static final InjuryType CONCUSSION = new InjuryTypes.Concussion(); public static final InjuryType BROKEN_RIB = new InjuryTypes.BrokenRib(); public static final InjuryType BRUISED_KIDNEY = new InjuryTypes.BruisedKidney(); public static final InjuryType BROKEN_LIMB = new InjuryTypes.BrokenLimb(); public static final InjuryType BROKEN_COLLAR_BONE = new InjuryTypes.BrokenCollarBone(); public static final InjuryType INTERNAL_BLEEDING = new InjuryTypes.InternalBleeding(); public static final InjuryType LOST_LIMB = new InjuryTypes.LostLimb(); public static final InjuryType CEREBRAL_CONTUSION = new InjuryTypes.CerebralContusion(); public static final InjuryType PUNCTURED_LUNG = new InjuryTypes.PuncturedLung(); public static final InjuryType CTE = new InjuryTypes.Cte(); public static final InjuryType BROKEN_BACK = new InjuryTypes.BrokenBack(); // New injury types go here (or extend the class) public static final InjuryType SEVERED_SPINE = new InjuryTypes.SeveredSpine(); private static boolean registered = false; /** Register all injury types defined here. Don't use them until you called this once! */ public static synchronized void registerAll() { if(!registered) { InjuryType.register(0, "am:cut", CUT); InjuryType.register(1, "am:bruise", BRUISE); InjuryType.register(2, "am:laceration", LACERATION); InjuryType.register(3, "am:sprain", SPRAIN); InjuryType.register(4, "am:concussion", CONCUSSION); InjuryType.register(5, "am:broken_rib", BROKEN_RIB); InjuryType.register(6, "am:bruised_kidney", BRUISED_KIDNEY); InjuryType.register(7, "am:broken_limb", BROKEN_LIMB); InjuryType.register(8, "am:broken_collar_bone", BROKEN_COLLAR_BONE); InjuryType.register(9, "am:internal_bleeding", INTERNAL_BLEEDING); InjuryType.register(10, "am:lost_limb", LOST_LIMB); InjuryType.register(11, "am:cerebral_contusion", CEREBRAL_CONTUSION); InjuryType.register(12, "am:punctured_lung", PUNCTURED_LUNG); InjuryType.register(13, "am:cte", CTE); InjuryType.register(14, "am:broken_back", BROKEN_BACK); InjuryType.register("am:severed_spine", SEVERED_SPINE); registered = true; } } private static class AMInjuryType extends InjuryType { protected int modifyInjuryTime(Person p, int time) { // Randomize healing time int mod = 100; int rand = Compute.randomInt(100); if(rand < 5) { mod += (Compute.d6() < 4) ? rand : -rand; } return Math.round(time * mod * p.getAbilityTimeModifier() / 10000); } @Override public Injury newInjury(Campaign c, Person p, BodyLocation loc, int severity) { Injury result = super.newInjury(c, p, loc, severity); final int time = modifyInjuryTime(p, result.getOriginalTime()); result.setOriginalTime(time); result.setTime(time); return result; } } public static final class SeveredSpine extends AMInjuryType { public SeveredSpine() { recoveryTime = 180; allowedLocations = EnumSet.of(BodyLocation.CHEST, BodyLocation.ABDOMEN); permanent = true; fluffText = "A severed spine"; simpleName = "severed spine"; level = InjuryLevel.CHRONIC; } @Override public String getFluffText(BodyLocation loc, int severity, int gender) { return "A severed spine in " + ((loc == BodyLocation.CHEST) ? "upper" : "lower") + " body"; } @Override public Collection<Modifier> getModifiers(Injury inj) { return Arrays.asList(new Modifier(Modifier.Value.PILOTING, Integer.MAX_VALUE, null, InjuryType.MODTAG_INJURY)); } } public static final class BrokenBack extends AMInjuryType { public BrokenBack() { recoveryTime = 150; allowedLocations = EnumSet.of(BodyLocation.CHEST); fluffText = "A broken back"; simpleName = "broken back"; level = InjuryLevel.MAJOR; } @Override public List<GameEffect> genStressEffect(Campaign c, Person p, Injury i, int hits) { return Arrays.asList(new GameEffect( "20% chance of severing the spine, permanently paralizing the character", rnd -> { if(rnd.applyAsInt(100) < 20) { Injury severedSpine = SEVERED_SPINE.newInjury(c, p, BodyLocation.CHEST, 1); p.addInjury(severedSpine); LogEntry entry = new LogEntry(c.getDate(), "Severed " + Person.getGenderPronoun(p.getGender(), Person.PRONOUN_HISHER) + " spine, leaving " + Person.getGenderPronoun(p.getGender(), Person.PRONOUN_HIMHER) + " paralyzed", Person.LOGTYPE_MEDICAL); p.addLogEntry(entry); MekHQ.logMessage(entry.toString()); } })); } @Override public Collection<Modifier> getModifiers(Injury inj) { return Arrays.asList( new Modifier(Modifier.Value.GUNNERY, 3, null, InjuryType.MODTAG_INJURY), new Modifier(Modifier.Value.PILOTING, 3, null, InjuryType.MODTAG_INJURY)); } } public static final class Cte extends AMInjuryType { public Cte() { recoveryTime = 180; allowedLocations = EnumSet.of(BodyLocation.HEAD); permanent = true; fluffText = "Chronic traumatic encephalopathy"; simpleName = "CTE"; level = InjuryLevel.DEADLY; } @Override public List<GameEffect> genStressEffect(Campaign c, Person p, Injury i, int hits) { int deathchance = Math.max((int) Math.round((1 + hits) * 100.0 / 6.0), 100); if(hits > 4) { return Arrays.asList( new GameEffect( "certain death", rnd -> { p.setStatus(Person.S_KIA); LogEntry entry = new LogEntry(c.getDate(), "Died due to brain trauma", Person.LOGTYPE_MEDICAL); p.addLogEntry(entry); MekHQ.logMessage(entry.toString()); })); } else { // We have a chance! return Arrays.asList( newResetRecoveryTimeAction(i), new GameEffect( deathchance + "% chance of death", rnd -> { if(rnd.applyAsInt(6) + hits >= 5) { p.setStatus(Person.S_KIA); LogEntry entry = new LogEntry(c.getDate(), "Died due to brain trauma", Person.LOGTYPE_MEDICAL); p.addLogEntry(entry); MekHQ.logMessage(entry.toString()); } })); } } @Override public Collection<Modifier> getModifiers(Injury inj) { return Arrays.asList(new Modifier(Modifier.Value.PILOTING, Integer.MAX_VALUE, null, InjuryType.MODTAG_INJURY)); } } public static final class PuncturedLung extends AMInjuryType { public PuncturedLung() { recoveryTime = 20; allowedLocations = EnumSet.of(BodyLocation.CHEST); fluffText = "A punctured lung"; simpleName = "punctured lung"; level = InjuryLevel.MAJOR; } @Override public List<GameEffect> genStressEffect(Campaign c, Person p, Injury i, int hits) { return Arrays.asList(newResetRecoveryTimeAction(i)); } } public static final class CerebralContusion extends AMInjuryType { public CerebralContusion() { recoveryTime = 90; allowedLocations = EnumSet.of(BodyLocation.HEAD); fluffText = "A cerebral contusion"; simpleName = "cerebral contusion"; level = InjuryLevel.MAJOR; } @Override public List<GameEffect> genStressEffect(Campaign c, Person p, Injury i, int hits) { String secondEffectFluff = "development of a chronic traumatic encephalopathy"; if(hits < 5) { int worseningChance = Math.max((int) Math.round((1 + hits) * 100.0 / 6.0), 100); secondEffectFluff = worseningChance + "% chance of " + secondEffectFluff; } return Arrays.asList( newResetRecoveryTimeAction(i), new GameEffect( secondEffectFluff, rnd -> { if(rnd.applyAsInt(6) + hits >= 5) { Injury cte = CTE.newInjury(c, p, BodyLocation.HEAD, 1); p.addInjury(cte); p.removeInjury(i); LogEntry entry = new LogEntry(c.getDate(), "Developed a chronic traumatic encephalopathy", Person.LOGTYPE_MEDICAL); p.addLogEntry(entry); MekHQ.logMessage(entry.toString()); } }) ); } @Override public Collection<Modifier> getModifiers(Injury inj) { return Arrays.asList(new Modifier(Modifier.Value.PILOTING, 2, null, InjuryType.MODTAG_INJURY)); } } public static final class LostLimb extends AMInjuryType { public LostLimb() { recoveryTime = 28; permanent = true; simpleName = "lost"; level = InjuryLevel.CHRONIC; } @Override public boolean isValidInLocation(BodyLocation loc) { return loc.isLimb; } @Override public boolean impliesMissingLocation(BodyLocation loc) { return true; } @Override public String getName(BodyLocation loc, int severity) { return "Missing " + Utilities.capitalize(loc.readableName); } @Override public String getFluffText(BodyLocation loc, int severity, int gender) { return "Lost " + Person.getGenderPronoun(gender, Person.PRONOUN_HISHER) + " " + loc.readableName; } @Override public Collection<Modifier> getModifiers(Injury inj) { BodyLocation loc = inj.getLocation(); switch(loc) { case LEFT_ARM: case LEFT_HAND: case RIGHT_ARM: case RIGHT_HAND: return Arrays.asList(new Modifier(Modifier.Value.GUNNERY, 3, null, InjuryType.MODTAG_INJURY)); case LEFT_LEG: case LEFT_FOOT: case RIGHT_LEG: case RIGHT_FOOT: return Arrays.asList(new Modifier(Modifier.Value.PILOTING, 3, null, InjuryType.MODTAG_INJURY)); default: return Arrays.asList(); } } } public static final class InternalBleeding extends AMInjuryType { public InternalBleeding() { recoveryTime = 20; allowedLocations = EnumSet.of(BodyLocation.ABDOMEN, BodyLocation.INTERNAL); maxSeverity = 3; simpleName = "internal bleeding"; } @Override public int getRecoveryTime(int severity) { return 20 * severity; } @Override public String getName(BodyLocation loc, int severity) { return Utilities.capitalize(getFluffText(loc, severity, 0)); } @Override public InjuryLevel getLevel(Injury i) { return (i.getHits() > 2) ? InjuryLevel.DEADLY : InjuryLevel.MAJOR; } @Override public String getFluffText(BodyLocation loc, int severity, int gender) { switch(severity) { case 2: return "Severe internal bleeding"; case 3: return "Critical internal bleeding"; default: return "Internal bleeding"; } } @Override public String getSimpleName(int severity) { switch(severity) { case 2: return "internal bleeding (severe)"; case 3: return "internal bleeding (critical)"; default: return "internal bleeding"; } } @Override public List<GameEffect> genStressEffect(Campaign c, Person p, Injury i, int hits) { String secondEffectFluff = (i.getHits() < 3) ? "internal bleeding worsening" : "death"; if(hits < 5) { int worseningChance = Math.max((int) Math.round((1 + hits) * 100.0 / 6.0), 100); secondEffectFluff = worseningChance + "% chance of " + secondEffectFluff; } if(hits >= 5 && i.getHits() >= 3) { // Don't even bother doing anything else; we're dead return Arrays.asList( new GameEffect( "certain death", rnd -> { p.setStatus(Person.S_KIA); LogEntry entry = new LogEntry(c.getDate(), "Died of critical internal bleeding", Person.LOGTYPE_MEDICAL); p.addLogEntry(entry); MekHQ.logMessage(entry.toString()); }) ); } else { // We have a chance! return Arrays.asList( newResetRecoveryTimeAction(i), new GameEffect( secondEffectFluff, rnd -> { if(rnd.applyAsInt(6) + hits >= 5) { if(i.getHits() < 3) { i.setHits(i.getHits() + 1); LogEntry entry = new LogEntry(c.getDate(), "Internal bleeding worsened", Person.LOGTYPE_MEDICAL); p.addLogEntry(entry); MekHQ.logMessage(entry.toString()); } else { p.setStatus(Person.S_KIA); LogEntry entry = new LogEntry(c.getDate(), "Died of critical internal bleeding", Person.LOGTYPE_MEDICAL); p.addLogEntry(entry); MekHQ.logMessage(entry.toString()); } } }) ); } } } public static final class BrokenCollarBone extends AMInjuryType { public BrokenCollarBone() { recoveryTime = 22; allowedLocations = EnumSet.of(BodyLocation.CHEST); fluffText = "A broken collar bone"; simpleName = "broken collar bone"; level = InjuryLevel.MAJOR; } @Override public List<GameEffect> genStressEffect(Campaign c, Person p, Injury i, int hits) { return Arrays.asList(newResetRecoveryTimeAction(i)); } } public static final class BrokenLimb extends AMInjuryType { public BrokenLimb() { recoveryTime = 30; simpleName = "broken"; level = InjuryLevel.MAJOR; } @Override public boolean isValidInLocation(BodyLocation loc) { return loc.isLimb; } @Override public String getName(BodyLocation loc, int severity) { return "Broken " + Utilities.capitalize(loc.readableName); } @Override public String getFluffText(BodyLocation loc, int severity, int gender) { return "A broken " + loc.readableName; } @Override public List<GameEffect> genStressEffect(Campaign c, Person p, Injury i, int hits) { return Arrays.asList(newResetRecoveryTimeAction(i)); } @Override public Collection<Modifier> getModifiers(Injury inj) { BodyLocation loc = inj.getLocation(); switch(loc) { case LEFT_ARM: case LEFT_HAND: case RIGHT_ARM: case RIGHT_HAND: return Arrays.asList(new Modifier(Modifier.Value.GUNNERY, inj.isPermanent() ? 1 : 2, null, InjuryType.MODTAG_INJURY)); case LEFT_LEG: case LEFT_FOOT: case RIGHT_LEG: case RIGHT_FOOT: return Arrays.asList(new Modifier(Modifier.Value.PILOTING, inj.isPermanent() ? 1 : 2, null, InjuryType.MODTAG_INJURY)); default: return Arrays.asList(); } } } public static final class BruisedKidney extends AMInjuryType { public BruisedKidney() { recoveryTime = 10; allowedLocations = EnumSet.of(BodyLocation.ABDOMEN); fluffText = "A bruised kidney"; simpleName = "bruised kidney"; level = InjuryLevel.MINOR; } @Override public List<GameEffect> genStressEffect(Campaign c, Person p, Injury i, int hits) { return Arrays.asList(new GameEffect( "10% chance of internal bleeding", rnd -> { if(rnd.applyAsInt(100) < 10) { Injury bleeding = INTERNAL_BLEEDING.newInjury(c, p, BodyLocation.ABDOMEN, 1); p.addInjury(bleeding); LogEntry entry = new LogEntry(c.getDate(), "Had a broken rib puncturing " + Person.getGenderPronoun(p.getGender(), Person.PRONOUN_HISHER) + " lung", Person.LOGTYPE_MEDICAL); p.addLogEntry(entry); MekHQ.logMessage(entry.toString()); } })); } } public static final class BrokenRib extends AMInjuryType { public BrokenRib() { recoveryTime = 20; allowedLocations = EnumSet.of(BodyLocation.CHEST); fluffText = "A broken rib"; simpleName = "broken rib"; level = InjuryLevel.MAJOR; } @Override public List<GameEffect> genStressEffect(Campaign c, Person p, Injury i, int hits) { return Arrays.asList(new GameEffect( "1% chance of death; 9% chance of puncturing a lung", rnd -> { int rib = rnd.applyAsInt(100); if(rib < 1) { p.changeStatus(Person.S_KIA); LogEntry entry = new LogEntry(c.getDate(), "Had a broken rib puncturing " + Person.getGenderPronoun(p.getGender(), Person.PRONOUN_HISHER) + " heart, dying", Person.LOGTYPE_MEDICAL); p.addLogEntry(entry); MekHQ.logMessage(entry.toString()); } else if(rib < 10) { Injury puncturedLung = PUNCTURED_LUNG.newInjury(c, p, BodyLocation.CHEST, 1); p.addInjury(puncturedLung); LogEntry entry = new LogEntry(c.getDate(), "Had a broken rib puncturing " + Person.getGenderPronoun(p.getGender(), Person.PRONOUN_HISHER) + " lung", Person.LOGTYPE_MEDICAL); p.addLogEntry(entry); MekHQ.logMessage(entry.toString()); } })); } } public static final class Concussion extends AMInjuryType { public Concussion() { recoveryTime = 14; allowedLocations = EnumSet.of(BodyLocation.HEAD); maxSeverity = 2; fluffText = "A concussion"; } @Override public int getRecoveryTime(int severity) { return severity >= 2 ? 42 : 14; } @Override public InjuryLevel getLevel(Injury i) { return (i.getHits() > 1) ? InjuryLevel.MAJOR : InjuryLevel.MINOR; } @Override public String getSimpleName(int severity) { return ((severity == 1) ? "concussion" : "concussion (severe)"); } @Override public List<GameEffect> genStressEffect(Campaign c, Person p, Injury i, int hits) { String secondEffectFluff = (i.getHits() == 1) ? "concussion worsening" : "development of a cerebral contusion"; if(hits < 5) { int worseningChance = Math.max((int) Math.round((1 + hits) * 100.0 / 6.0), 100); secondEffectFluff = worseningChance + "% chance of " + secondEffectFluff; } return Arrays.asList( newResetRecoveryTimeAction(i), new GameEffect( secondEffectFluff, rnd -> { if(rnd.applyAsInt(6) + hits >= 5) { if(i.getHits() == 1) { i.setHits(2); LogEntry entry = new LogEntry(c.getDate(), "Concussion worsened", Person.LOGTYPE_MEDICAL); p.addLogEntry(entry); MekHQ.logMessage(entry.toString()); } else { Injury cerebralContusion = CEREBRAL_CONTUSION.newInjury(c, p, BodyLocation.HEAD, 1); p.addInjury(cerebralContusion); p.removeInjury(i); LogEntry entry = new LogEntry(c.getDate(), "Developed a cerebral contusion", Person.LOGTYPE_MEDICAL); p.addLogEntry(entry); MekHQ.logMessage(entry.toString()); } } }) ); } @Override public Collection<Modifier> getModifiers(Injury inj) { return Arrays.asList(new Modifier(Modifier.Value.PILOTING, 1, null, InjuryType.MODTAG_INJURY)); } } public static final class Sprain extends AMInjuryType { public Sprain() { recoveryTime = 12; simpleName = "sprained"; level = InjuryLevel.MINOR; } @Override public boolean isValidInLocation(BodyLocation loc) { return loc.isLimb; } @Override public String getName(BodyLocation loc, int severity) { return "Sprained " + Utilities.capitalize(loc.readableName); } @Override public String getFluffText(BodyLocation loc, int severity, int gender) { return "A sprained " + loc.readableName; } @Override public Collection<Modifier> getModifiers(Injury inj) { BodyLocation loc = inj.getLocation(); switch(loc) { case LEFT_ARM: case LEFT_HAND: case RIGHT_ARM: case RIGHT_HAND: return Arrays.asList(new Modifier(Modifier.Value.GUNNERY, 1, null, InjuryType.MODTAG_INJURY)); case LEFT_LEG: case LEFT_FOOT: case RIGHT_LEG: case RIGHT_FOOT: return Arrays.asList(new Modifier(Modifier.Value.PILOTING, 1, null, InjuryType.MODTAG_INJURY)); default: return Arrays.asList(); } } } public static final class Laceration extends AMInjuryType { public Laceration() { allowedLocations = EnumSet.of(BodyLocation.HEAD); simpleName = "laceration"; level = InjuryLevel.MINOR; } @Override public String getName(BodyLocation loc, int severity) { return "Lacerated " + Utilities.capitalize(loc.readableName); } @Override public String getFluffText(BodyLocation loc, int severity, int gender) { return "A laceration on " + Person.getGenderPronoun(gender, Person.PRONOUN_HISHER) + " head"; } @Override protected int modifyInjuryTime(Person p, int time) { return super.modifyInjuryTime(p, time + Compute.d6()); } } public static final class Bruise extends AMInjuryType { public Bruise() { allowedLocations = EnumSet.of(BodyLocation.CHEST, BodyLocation.ABDOMEN); simpleName = "bruised"; level = InjuryLevel.MINOR; } @Override public boolean isValidInLocation(BodyLocation loc) { return loc.isLimb || super.isValidInLocation(loc); } @Override public String getName(BodyLocation loc, int severity) { return "Bruised " + Utilities.capitalize(loc.readableName); } @Override public String getFluffText(BodyLocation loc, int severity, int gender) { return "A bruise on " + Person.getGenderPronoun(gender, Person.PRONOUN_HISHER) + " " + loc.readableName; } @Override protected int modifyInjuryTime(Person p, int time) { return super.modifyInjuryTime(p, time + Compute.d6()); } } public static final class Cut extends AMInjuryType { public Cut() { allowedLocations = EnumSet.of(BodyLocation.CHEST, BodyLocation.ABDOMEN); simpleName = "cut"; level = InjuryLevel.MINOR; } @Override public boolean isValidInLocation(BodyLocation loc) { return loc.isLimb || super.isValidInLocation(loc); } @Override public String getName(BodyLocation loc, int severity) { return "Cut " + Utilities.capitalize(loc.readableName); } @Override public String getFluffText(BodyLocation loc, int severity, int gender) { return "Some cuts on " + Person.getGenderPronoun(gender, Person.PRONOUN_HISHER) + " " + loc.readableName; } @Override protected int modifyInjuryTime(Person p, int time) { return super.modifyInjuryTime(p, time + Compute.d6()); } } }