/*
* 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.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.IntUnaryOperator;
import megamek.common.Aero;
import megamek.common.Compute;
import megamek.common.Entity;
import megamek.common.Mech;
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.InjuryType;
import mekhq.campaign.personnel.Person;
import mekhq.campaign.personnel.Skill;
import mekhq.campaign.personnel.SkillType;
import mekhq.campaign.unit.Unit;
/**
* Static helper methods implementing the "advanced medical" sub-system
*/
public final class InjuryUtil {
// Fumble and critical success limits for doctor skills levels 0-10, on a d100
private static final int FUMBLE_LIMITS[] = {50, 40, 30, 20, 12, 6, 5, 4, 3, 2, 1};
private static final int CRIT_LIMITS[] = {98, 97, 94, 89, 84, 79, 74, 69, 64, 59, 49};
/*
private static AMEventHandler eventHandler = null;
public synchronized static void registerEventHandler(Campaign c) {
if(null != eventHandler) {
MekHQ.EVENT_BUS.unregister(eventHandler);
}
MekHQ.EVENT_BUS.register(eventHandler = new AMEventHandler(c));
}
*/
/** Run a daily healing check */
public static void resolveDailyHealing(Campaign c, Person p) {
Person doc = c.getPerson(p.getDoctorId());
// TODO: Reporting
if((null != doc) && doc.isDoctor()) {
if(p.getDaysToWaitForHealing() <= 0) {
genMedicalTreatment(c, p, doc).stream().forEach(GameEffect::apply);
}
} else {
genUntreatedEffects(c, p).stream().forEach(GameEffect::apply);
}
genNaturalHealing(c, p).stream().forEach(GameEffect::apply);
p.decrementDaysToWaitForHealing();
}
/** Resolve injury modifications in case of entering combat with active ones */
public static void resolveAfterCombat(Campaign c, Person p, int hits) {
// Gather all the injury actions resulting from the combat situation
final List<GameEffect> effects = new ArrayList<>();
p.getInjuries().forEach(i ->
{
effects.addAll(i.getType().genStressEffect(c, p, i, hits));
});
// We could do some fancy display-to-the-user thing here, but for now just resolve all actions
effects.stream().forEach(GameEffect::apply);
}
/** Resolve effects of damage suffered during combat */
public static void resolveCombatDamage(Campaign c, Person person, int hits) {
Collection<Injury> newInjuries = genInjuries(c, person, hits);
newInjuries.forEach((inj) -> person.addInjury(inj));
if (newInjuries.size() > 0) {
StringBuilder sb = new StringBuilder("Returned from combat with the following new injuries:");
newInjuries.forEach((inj) -> sb.append("\n\t\t").append(inj.getFluff()));
LogEntry entry = new LogEntry(c.getDate(), sb.toString(), Person.LOGTYPE_MEDICAL);
person.addLogEntry(entry);
}
}
private static void addHitToAccumulator(Map<BodyLocation, Integer> acc, BodyLocation loc) {
if(!acc.containsKey(loc)) {
acc.put(loc, Integer.valueOf(1));
} else {
acc.put(loc, acc.get(loc) + 1);
}
}
// Generator methods. Those don't change the state of the person.
/** Generate combat injuries spread through the whole body */
public static Collection<Injury> genInjuries(Campaign c, Person p, int hits) {
final Unit u = c.getUnit(p.getUnitId());
final Entity en = (null != u) ? u.getEntity() : null;
final boolean mwasf = (null != en) && ((en instanceof Mech) || (en instanceof Aero));
final int critMod = mwasf ? 0 : 2;
final BiFunction<IntUnaryOperator, Function<BodyLocation, Boolean>, BodyLocation> generator
= mwasf ? HitLocationGen::mechAndAsf : HitLocationGen::generic;
final Map<BodyLocation, Integer> hitAccumulator = new HashMap<>();
for (int i = 0; i < hits; i++) {
BodyLocation location
= generator.apply(Compute::randomInt, (loc) -> !p.isLocationMissing(loc));
// apply hit here
addHitToAccumulator(hitAccumulator, location);
// critical hits add to the amount
int roll = Compute.d6(2);
if(roll + hits + critMod > 12) {
addHitToAccumulator(hitAccumulator, location);
}
}
List<Injury> newInjuries = new ArrayList<>();
for(Entry<BodyLocation, Integer> accEntry : hitAccumulator.entrySet()) {
newInjuries.addAll(genInjuries(c, p, accEntry.getKey(), accEntry.getValue().intValue()));
}
return newInjuries;
}
/** Generate combat injuries for a specific body location */
public static Collection<Injury> genInjuries(Campaign c, Person p, BodyLocation loc, int hits) {
List<Injury> newInjuries = new ArrayList<Injury>();
final BiFunction<InjuryType, Integer, Injury> gen = (it, severity) -> {
return it.newInjury(c, p, loc, severity);
};
switch(loc) {
case LEFT_ARM: case LEFT_HAND: case LEFT_LEG: case LEFT_FOOT:
case RIGHT_ARM: case RIGHT_HAND: case RIGHT_LEG: case RIGHT_FOOT:
switch(hits) {
case 1:
newInjuries.add(gen.apply(
Compute.randomInt(2) == 0 ? InjuryTypes.CUT : InjuryTypes.BRUISE, 1));
break;
case 2:
newInjuries.add(gen.apply(InjuryTypes.SPRAIN, 1));
break;
case 3:
newInjuries.add(gen.apply(InjuryTypes.BROKEN_LIMB, 1));
break;
case 4:
newInjuries.add(gen.apply(InjuryTypes.LOST_LIMB, 1));
break;
}
break;
case HEAD:
switch(hits) {
case 1:
newInjuries.add(gen.apply(InjuryTypes.LACERATION, 1));
break;
case 2: case 3:
newInjuries.add(gen.apply(InjuryTypes.CONCUSSION, hits - 1));
break;
case 4:
newInjuries.add(gen.apply(InjuryTypes.CEREBRAL_CONTUSION, 1));
break;
default:
newInjuries.add(gen.apply(InjuryTypes.CTE, 1));
break;
}
break;
case CHEST:
switch(hits) {
case 1:
newInjuries.add(gen.apply(
Compute.randomInt(2) == 0 ? InjuryTypes.CUT : InjuryTypes.BRUISE, 1));
break;
case 2:
newInjuries.add(gen.apply(InjuryTypes.BROKEN_RIB, 1));
break;
case 3:
newInjuries.add(gen.apply(InjuryTypes.BROKEN_COLLAR_BONE, 1));
break;
case 4:
newInjuries.add(gen.apply(InjuryTypes.PUNCTURED_LUNG, 1));
break;
default:
newInjuries.add(gen.apply(InjuryTypes.BROKEN_BACK, 1));
if(Compute.randomInt(100) < 15) {
newInjuries.add(gen.apply(InjuryTypes.SEVERED_SPINE, 1));
}
break;
}
break;
case ABDOMEN:
switch(hits) {
case 1:
newInjuries.add(gen.apply(
Compute.randomInt(2) == 0 ? InjuryTypes.CUT : InjuryTypes.BRUISE, 1));
break;
case 2:
newInjuries.add(gen.apply(InjuryTypes.BRUISED_KIDNEY, 1));
break;
default:
newInjuries.add(gen.apply(InjuryTypes.INTERNAL_BLEEDING, hits - 2));
break;
}
break;
case GENERIC:
case INTERNAL:
// AM doesn't deal with those
break;
default:
break;
}
return newInjuries;
}
/** Called when creating a new injury to generate a slightly randomized healing time */
public static int genHealingTime(Campaign c, Person p, Injury i) {
return genHealingTime(c, p, i.getType(), i.getHits());
}
/** Called when creating a new injury to generate a slightly randomized healing time */
public static int genHealingTime(Campaign c, Person p, InjuryType itype, int severity) {
int mod = 100;
int rand = Compute.randomInt(100);
if(rand < 5) {
mod += (Compute.d6() < 4) ? rand : -rand;
}
int time = itype.getRecoveryTime(severity);
if(itype == InjuryTypes.LACERATION) {
time += Compute.d6();
}
time = Math.round(time * mod * p.getAbilityTimeModifier() / 10000);
return time;
}
/** Generate the effects of a doctor dealing with injuries (frequency depends on campaign settings) */
public static List<GameEffect> genMedicalTreatment(Campaign c, Person p, Person doc) {
Objects.requireNonNull(c);
Objects.requireNonNull(p);
Skill skill = doc.getSkill(SkillType.S_DOCTOR);
int level = skill.getLevel();
final int fumbleLimit = FUMBLE_LIMITS[(level >= 0) && (level <= 10) ? level : 0];
final int critLimt = CRIT_LIMITS[(level >= 0) && (level <= 10) ? level : 0];
int xpGained = 0;
int mistakeXP = 0;
int successXP = 0;
int numTreated = 0;
int numResting = 0;
List<GameEffect> result = new ArrayList<>();
for(Injury i : p.getInjuries()) {
if(!i.isWorkedOn()) {
int roll = Compute.randomInt(100);
// Determine XP, if any
if (roll < Math.max(1, fumbleLimit / 10)) {
mistakeXP += c.getCampaignOptions().getMistakeXP();
xpGained += mistakeXP;
} else if (roll > Math.min(98, 99 - Math.round(99 - critLimt) / 10)) {
successXP += c.getCampaignOptions().getSuccessXP();
xpGained += successXP;
}
final int critTimeReduction = i.getTime() - (int) Math.floor(i.getTime() * 0.9);
if(roll < fumbleLimit) {
result.add(new GameEffect(
String.format("%s made a mistake in the treatment of %s and caused %s %s to worsen.",
doc.getHyperlinkedFullTitle(), p.getHyperlinkedName(),
p.getGenderPronoun(Person.PRONOUN_HISHER), i.getName()),
rnd -> {
int time = i.getTime();
i.setTime((int) Math.max(Math.ceil(time * 1.2), time + 5));
LogEntry entry = new LogEntry(c.getDate(),
String.format("%s made a mistake and caused %s to worsen.",
doc.getFullTitle(), i.getName()),
Person.LOGTYPE_MEDICAL);
p.addLogEntry(entry);
if(rnd.applyAsInt(100) < (fumbleLimit / 4)) {
// TODO: Add in special handling of the critical
// injuries like broken back (make perm),
// broken ribs (punctured lung/death chance) internal
// bleeding (death chance)
}
}));
} else if((roll > critLimt) && (critTimeReduction > 0)) {
result.add(new GameEffect(
String.format("%s performed some amazing work in treating %s of %s (%d fewer day(s) to heal).",
doc.getHyperlinkedFullTitle(), i.getName(), p.getHyperlinkedName(), critTimeReduction),
rnd -> {
i.setTime(i.getTime() - critTimeReduction);
LogEntry entry = new LogEntry(c.getDate(),
String.format("%s performed some amazing work in treating %s (%d fewer day(s) to heal).",
doc.getFullTitle(), i.getName(), critTimeReduction),
Person.LOGTYPE_MEDICAL);
p.addLogEntry(entry);
}));
} else {
final int xpChance = (int) Math.round(100.0 / c.getCampaignOptions().getNTasksXP());
result.add(new GameEffect(
String.format("%s successfully treated %s [%d%% chance of gaining %d XP]",
doc.getHyperlinkedFullTitle(), p.getHyperlinkedName(),
xpChance, c.getCampaignOptions().getTaskXP()),
rnd -> {
int taskXP = c.getCampaignOptions().getTaskXP();
if((taskXP > 0) && (doc.getNTasks() >= c.getCampaignOptions().getNTasksXP())) {
doc.setXp(doc.getXp() + taskXP);
doc.setNTasks(0);
LogEntry docEntry = new LogEntry(c.getDate(),
String.format("Gained %d XP from successful medical work.", taskXP));
doc.addLogEntry(docEntry);
} else {
doc.setNTasks(doc.getNTasks() + 1);
}
i.setWorkedOn(true);
LogEntry entry = new LogEntry(c.getDate(),
String.format("%s successfully treated %s.",
doc.getFullTitle(), i.getName()),
Person.LOGTYPE_MEDICAL);
p.addLogEntry(entry);
Unit u = c.getUnit(p.getUnitId());
if(null != u) {
u.resetPilotAndEntity();
}
}));
}
i.setWorkedOn(true);
Unit u = c.getUnit(p.getUnitId());
if(null != u) {
u.resetPilotAndEntity();
}
numTreated++;
} else {
result.add(new GameEffect(
String.format("%s spent time resting to heal %s %s.",
p.getHyperlinkedName(), p.getGenderPronoun(Person.PRONOUN_HISHER), i.getName()),
rnd -> {
}));
numResting++;
}
}
if (numTreated > 0) {
final int xp = xpGained;
final int injuries = numTreated;
final String treatmentSummary = (xpGained > 0)
? String.format("%s successfully treated %s for %d injuries "
+ "(%d XP gained, %d for mistakes, %d for critical successes, and %d for tasks).",
doc.getHyperlinkedFullTitle(), p.getHyperlinkedName(), numTreated,
xp, mistakeXP, successXP, xp - mistakeXP - successXP)
: String.format("%s successfully treated %s for %d injuries.",
doc.getHyperlinkedFullTitle(), p.getHyperlinkedName(), numTreated);
result.add(new GameEffect(treatmentSummary,
rnd -> {
if(xp > 0) {
doc.setXp(doc.getXp() + xp);
LogEntry entry = new LogEntry(c.getDate(),
String.format("Successfully treated %s for %d injuries, gaining %d XP",
p.getName(), injuries, xp));
doc.addLogEntry(entry);
} else {
LogEntry entry = new LogEntry(c.getDate(),
String.format("Successfully treated %s for %d injuries",
p.getName(), injuries));
doc.addLogEntry(entry);
}
p.setDaysToWaitForHealing(c.getCampaignOptions().getHealingWaitingPeriod());
}));
}
if (numResting > 0) {
result.add(new GameEffect(
String.format("%s spent time resting to heal %d injuries.",
p.getHyperlinkedName(), numResting)));
}
return result;
}
/** Generate the effects of "natural" healing (daily) */
public static List<GameEffect> genNaturalHealing(Campaign c, Person p) {
Objects.requireNonNull(c);
Objects.requireNonNull(p);
List<GameEffect> result = new ArrayList<>();
p.getInjuries().forEach((i) -> {
if(i.getTime() <= 1 && !i.isPermanent()) {
InjuryType type = i.getType();
if(!i.isWorkedOn() &&
((type == InjuryTypes.BROKEN_LIMB) || (type == InjuryTypes.SPRAIN)
|| (type == InjuryTypes.CONCUSSION) || (type == InjuryTypes.BROKEN_COLLAR_BONE))) {
result.add(new GameEffect(
String.format("83%% chance of %s healing, 17%% chance of it becoming permanent.",
i.getName()),
rnd -> {
i.setTime(0);
if(rnd.applyAsInt(6) == 0) {
i.setPermanent(true);
LogEntry entry = new LogEntry(c.getDate(),
String.format("%s didn't heal properly", i.getName()), Person.LOGTYPE_MEDICAL);
p.addLogEntry(entry);
} else {
p.removeInjury(i);
LogEntry entry = new LogEntry(c.getDate(),
String.format("%s healed", i.getName()), Person.LOGTYPE_MEDICAL);
p.addLogEntry(entry);
}
}));
} else {
result.add(new GameEffect(
String.format("%s heals", i.getName()),
rnd -> {
i.setTime(0);
p.removeInjury(i);
LogEntry entry = new LogEntry(c.getDate(),
String.format("%s healed", i.getName()), Person.LOGTYPE_MEDICAL);
p.addLogEntry(entry);
}));
}
} else if(i.getTime() > 1) {
result.add(new GameEffect(
String.format("%s continues healing", i.getName()),
rnd -> {
i.setTime(Math.max(i.getTime() - 1, 0));
}));
} else if((i.getTime() == 1) && i.isPermanent()) {
result.add(new GameEffect(
String.format("%s becomes permanent", i.getName()),
rnd -> {
i.setTime(0);
LogEntry entry = new LogEntry(c.getDate(),
String.format("%s became a permanent injury", i.getName()), Person.LOGTYPE_MEDICAL);
p.addLogEntry(entry);
}));
}
});
if(null != p.getDoctorId()) {
result.add(new GameEffect("Infirmary health check-up",
rnd -> {
boolean dismissed = false;
if(p.getStatus() == Person.S_KIA) {
dismissed = true;
LogEntry entry = new LogEntry(c.getDate(), "Died in the infirmary", Person.LOGTYPE_MEDICAL);
p.addLogEntry(entry);
} else if(p.getStatus() == Person.S_MIA) {
// What? How?
dismissed = true;
LogEntry entry = new LogEntry(c.getDate(), "Got abducted from the infirmary", Person.LOGTYPE_MEDICAL);
p.addLogEntry(entry);
} else if(p.getStatus() == Person.S_RETIRED) {
dismissed = true;
LogEntry entry = new LogEntry(c.getDate(), "Retired from active duty and got transferred out of the infirmary", Person.LOGTYPE_MEDICAL);
p.addLogEntry(entry);
} else if(!p.needsFixing()) {
dismissed = true;
LogEntry entry = new LogEntry(c.getDate(), "Got dismissed from the infirmary", Person.LOGTYPE_MEDICAL);
p.addLogEntry(entry);
}
if(dismissed) {
p.setDoctorId(null, c.getCampaignOptions().getHealingWaitingPeriod());
}
}));
}
return result;
}
/** Generate the effects not being under proper treatment (daily) */
public static List<GameEffect> genUntreatedEffects(Campaign c, Person p) {
Objects.requireNonNull(c);
Objects.requireNonNull(p);
List<GameEffect> result = new ArrayList<>();
p.getInjuries().forEach((i) -> {
if((i.getTime() > 0) && !i.isPermanent() && !i.isWorkedOn()) {
result.add(new GameEffect(
String.format("30%% chance of %s worsening its condition", i.getName()),
rnd -> {
if(rnd.applyAsInt(100) < 30) {
i.setTime(i.getTime() + 1);
// TODO: Disabled, too much spam
/*
LogEntry entry = new LogEntry(c.getDate(),
String.format("%s worsened its condition due to lack of proper medical attention", i.getName()), Person.LOGTYPE_MEDICAL);
p.addLogEntry(entry);
*/
}
}));
}
});
return result;
}
}