diff --git a/MekHQ/data/CamOpsContractData2.xml b/MekHQ/data/CamOpsContractData2.xml deleted file mode 100644 index 9786e5467f..0000000000 --- a/MekHQ/data/CamOpsContractData2.xml +++ /dev/null @@ -1,131 +0,0 @@ - - - - - SuperPower - SUPER - - - - - Great - - 3 - 2 - 2 - - - - - - 0 - 0 - - - - - 2 - SmallState - - - - - 2 - Individual - - - - - SuperPower - Major - - - - - Major - - - - 2 - 1, 1 - - - - - - - - 1 - - false - 0 - 0 - 0 - 0 - 0 - 0 - 0 - - - - - - SuperPower - - false - 0 - 0 - 0 - 0 - 0 - 0 - 0 - - - - - - 0 - - false - 0 - 0 - 0 - 0 - 0 - 0 - 0 - - - - - - 2 - 0 - - - - - 2 - 0 - - - - - 2 - 0 - - - - - 2 - 0 - - - - - 2 - 0 - - - diff --git a/MekHQ/data/universe/atbconfig.xml b/MekHQ/data/universe/atbconfig.xml index 6ce1e38f55..c9cab40b18 100644 --- a/MekHQ/data/universe/atbconfig.xml +++ b/MekHQ/data/universe/atbconfig.xml @@ -138,28 +138,32 @@ of 4. An entry of the form option has a weight of 1. *Note that start and end dates are optional, but must follow yyyy-MM-dd format. Planet names much match the name in planets.xml exactly.--> - - Outreach - Solaris - Arc-Royal - Fletcher - Galatea - Westerhand - Northwind - Herotitus - - - - - - - - 100000 - - - 4 - - + + + 100000 + + + 4 + + diff --git a/MekHQ/data/universe/factions.xml b/MekHQ/data/universe/factions.xml index 6c57d22f2e..030c3ddb2a 100644 --- a/MekHQ/data/universe/factions.xml +++ b/MekHQ/data/universe/factions.xml @@ -50,7 +50,7 @@ layeredForceIconLogoCategory - the category of the faction's logo piece layeredForceIconLogoFilename - the filename of the faction's logo piece tags - a comma-separated list of tags. Currently recognised tags: "is", "periphery", "deep_periphery", "clan", "pirate", "merc", "trader", "minor", "rebel", "inactive", "hidden", -"abandoned", "chaos", "playable", "super" +"abandoned", "chaos", "playable", "super", "noble", "planetary_government", "corporation", "small" start - the founding date of the faction end - the date the faction ceases to exist successor - unimplemented tag describing another faction code as the specified faction's successor diff --git a/MekHQ/resources/mekhq/resources/AtBConfigDefaults.properties b/MekHQ/resources/mekhq/resources/AtBConfigDefaults.properties index d952b4cdc3..2299b46c2e 100644 --- a/MekHQ/resources/mekhq/resources/AtBConfigDefaults.properties +++ b/MekHQ/resources/mekhq/resources/AtBConfigDefaults.properties @@ -10,7 +10,7 @@ botLance.CLAN=1:LLLLL,1:LLLLM,1:LLLMM|1:LLMMM,1:LMMMM,2:MMMMM,1:MMMMH,1:MMMHH|1: botLance.CS=1:LLLLLL,2:LLLLLM,2:LLLLMM,1:LLLMMM|1:LLLMMM,1:LLMMMM,1:LMMMMM,1:MMMMMM,1:MMMMMH,1:MMMMHH|1:MMMHHH,1:MMHHHH,1:MHHHHH,1:HHHHHH,1:HHHHHA,1:HHHHAA|2:HHHAAA,1:HHAAAA,2:HAAAAA,1:AAAAAA #The following are not required for AtB to function and are only used if atbconfig.xml is missing or broken -hiringHalls=3031-01-01,3067-10-15,Outreach|2700-01-01,,Solaris|3057-01-01,,Arc-Royal|3058-01-01,3081-03-15,Fletcher|2650-01-01,,Galatea|3000-01-01,,Westerhand|3057-01-01,3081-03-15,Northwind|3020-01-01,,Herotitus +hiringHalls=3031-01-01,3067-10-15,great,Outreach|2700-01-01,,minor,Solaris|3057-01-01,,standard,Arc-Royal|3058-01-01,3081-03-15,minor,Fletcher|2650-01-01,,great,Galatea|3000-01-01,,great,Westerhand|3057-01-01,3081-03-15,great,Northwind|3020-01-01,,minor,Herotitus|2694-01-01,,questionable,Antallos (Port Krin)|2912-01-01,,questionable,Astrokaszy|3052-01-01,,minor,Noisiel|2811-01-01,3045-01-01,minor,Le Blanc shipSearchCost=100000 shipSearchLengthWeeks=4 diff --git a/MekHQ/resources/mekhq/resources/Market.properties b/MekHQ/resources/mekhq/resources/Market.properties index 7dc5e76dd8..1d1ee337fa 100644 --- a/MekHQ/resources/mekhq/resources/Market.properties +++ b/MekHQ/resources/mekhq/resources/Market.properties @@ -5,6 +5,8 @@ ContractMarketMethod.NONE.text=Disabled ContractMarketMethod.NONE.toolTipText=The Contract Market is disabled. ContractMarketMethod.ATB_MONTHLY.text=AtB Monthly ContractMarketMethod.ATB_MONTHLY.toolTipText=This is the standard Against the Bot Contract Market, which refreshes monthly. +ContractMarketMethod.CAM_OPS.text=Campaign Ops (Under Development) +ContractMarketMethod.CAM_OPS.toolTipText=This is the standard Campaign Operations Contract Market, which refreshes monthly. This feature is currently under development and may not work properly. # UnitMarketMethod Enum UnitMarketMethod.NONE.text=Disabled diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 213b694e95..0c4448bda2 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -59,10 +59,9 @@ import mekhq.campaign.log.HistoricalLogEntry; import mekhq.campaign.log.LogEntry; import mekhq.campaign.log.ServiceLogger; -import mekhq.campaign.market.ContractMarket; -import mekhq.campaign.market.PartsStore; -import mekhq.campaign.market.PersonnelMarket; -import mekhq.campaign.market.ShoppingList; +import mekhq.campaign.market.*; +import mekhq.campaign.market.contractMarket.AbstractContractMarket; +import mekhq.campaign.market.contractMarket.AtbMonthlyContractMarket; import mekhq.campaign.market.unitMarket.AbstractUnitMarket; import mekhq.campaign.market.unitMarket.DisabledUnitMarket; import mekhq.campaign.mission.*; @@ -210,8 +209,7 @@ public class Campaign implements ITechManager { private Boolean fieldKitchenWithinCapacity; - // this is updated and used per gaming session, it is enabled/disabled via the - // Campaign options + // this is updated and used per gaming session, it is enabled/disabled via the Campaign options // we're re-using the LogEntry class that is used to store Personnel entries public LinkedList inMemoryLogHistory = new LinkedList<>(); @@ -240,7 +238,7 @@ public class Campaign implements ITechManager { private ShoppingList shoppingList; private PersonnelMarket personnelMarket; - private ContractMarket contractMarket; // AtB + private AbstractContractMarket contractMarket; private AbstractUnitMarket unitMarket; private transient AbstractDeath death; @@ -319,7 +317,7 @@ public Campaign() { shoppingList = new ShoppingList(); news = new News(getGameYear(), id.getLeastSignificantBits()); setPersonnelMarket(new PersonnelMarket()); - setContractMarket(new ContractMarket()); + setContractMarket(new AtbMonthlyContractMarket()); setUnitMarket(new DisabledUnitMarket()); setDeath(new DisabledRandomDeath(getCampaignOptions(), false)); setDivorce(new DisabledRandomDivorce(getCampaignOptions())); @@ -465,13 +463,11 @@ public void setPersonnelMarket(final PersonnelMarket personnelMarket) { this.personnelMarket = personnelMarket; } - // TODO : AbstractContractMarket : Swap to AbstractContractMarket - public ContractMarket getContractMarket() { + public AbstractContractMarket getContractMarket() { return contractMarket; } - // TODO : AbstractContractMarket : Swap to AbstractContractMarket - public void setContractMarket(final ContractMarket contractMarket) { + public void setContractMarket(final AbstractContractMarket contractMarket) { this.contractMarket = contractMarket; } @@ -1662,8 +1658,7 @@ public boolean recruitPerson(Person p, PrisonerStatus prisonerStatus, boolean gm * appropriate to the person's phenotype and the player's faction. * * @param person The Bloodname candidate - * @param ignoreDice If true, skips the random roll and assigns a Bloodname - * automatically + * @param ignoreDice If true, skips the random roll and assigns a Bloodname automatically */ public void checkBloodnameAdd(Person person, boolean ignoreDice) { // if person is non-clan or does not have a phenotype @@ -1671,8 +1666,7 @@ public void checkBloodnameAdd(Person person, boolean ignoreDice) { return; } - // Person already has a bloodname, we open up the dialog to ask if they want to - // keep the + // Person already has a bloodname, we open up the dialog to ask if they want to keep the // current bloodname or assign a new one if (!person.getBloodname().isEmpty()) { int result = JOptionPane.showConfirmDialog(null, @@ -2221,6 +2215,18 @@ public Person findBestInRole(PersonnelRole role, String skill) { return findBestInRole(role, skill, null); } + public @Nullable Person findBestAtSkill(String skill) { + Person person = null; + int highest = 0; + for (Person p : getActivePersonnel()) { + if (p.getSkill(skill) != null && p.getSkill(skill).getLevel() > highest) { + highest = p.getSkill(skill).getLevel(); + person = p; + } + } + return person; + } + /** * @return The list of all active {@link Person}s who qualify as technicians * ({@link Person#isTech()})); @@ -3512,7 +3518,7 @@ && getLocation().getJumpPath().getLastSystem().getId().equals(contract.getSystem } private void processNewDayATB() { - contractMarket.generateContractOffers(this); // TODO : AbstractContractMarket : Remove + contractMarket.generateContractOffers(this); if ((getShipSearchExpiration() != null) && !getShipSearchExpiration().isAfter(getLocalDate())) { setShipSearchExpiration(null); @@ -6501,6 +6507,28 @@ public int getAtBUnitRatingMod() { : reputation.getAtbModifier(); } + public int getReputationFactor() { + return switch (campaignOptions.getUnitRatingMethod()) { + case NONE -> 5; + case FLD_MAN_MERCS_REV -> getAtBUnitRatingMod() * 2; + case CAMPAIGN_OPS -> (int) ((getReputation().getReputationRating() * 0.2) + 0.5); + }; + } + + /** + * Returns the Strategy skill of the designated commander in the campaign. + * + * @return The value of the commander's strategy skill if a commander exists, otherwise 0. + */ + public int getCommanderStrategy() { + int cmdrStrategy = 0; + if (getFlaggedCommander() != null && + getFlaggedCommander().getSkill(SkillType.S_STRATEGY) != null) { + cmdrStrategy = getFlaggedCommander().getSkill(SkillType.S_STRATEGY).getLevel(); + } + return cmdrStrategy; + } + @Deprecated public int getUnitRatingAsInteger() { return getAtBUnitRatingMod(); @@ -7560,8 +7588,7 @@ public void initTimeInRank() { LocalDate join = null; for (LogEntry e : p.getPersonnelLog()) { if (join == null) { - // If by some nightmare there is no date from the below, just use the first - // entry. + // If by some nightmare there is no date from the below, just use the first entry. join = e.getDate(); } @@ -7571,8 +7598,7 @@ public void initTimeInRank() { } } - // For that one in a billion chance the log is empty. Clone today's date and - // subtract a year + // For that one in a billion chance the log is empty. Clone today's date and subtract a year p.setLastRankChangeDate((join != null) ? join : getLocalDate().minusYears(1)); } } @@ -7684,8 +7710,7 @@ public boolean checkOverDueLoans() { } /** - * Checks if a turnover prompt should be displayed based on campaign options and - * current date. + * Checks if a turnover prompt should be displayed based on campaign options and current date. * * @return An integer representing the user's choice: * -1 if turnover prompt should not be displayed. @@ -7747,7 +7772,8 @@ public int checkTurnoverPrompt() { JOptionPane.INFORMATION_MESSAGE, null, options, - options[0]); + options[0] + ); } /** diff --git a/MekHQ/src/mekhq/campaign/againstTheBot/AtBConfiguration.java b/MekHQ/src/mekhq/campaign/againstTheBot/AtBConfiguration.java index 225119ece7..0623f56485 100644 --- a/MekHQ/src/mekhq/campaign/againstTheBot/AtBConfiguration.java +++ b/MekHQ/src/mekhq/campaign/againstTheBot/AtBConfiguration.java @@ -25,6 +25,10 @@ import megamek.common.annotations.Nullable; import megamek.logging.MMLogger; import mekhq.MekHQ; +import mekhq.campaign.universe.HiringHall; +import mekhq.campaign.universe.Planet; +import mekhq.campaign.universe.enums.HiringHallLevel; +import mekhq.utilities.MHQXMLUtility; import mekhq.campaign.Campaign; import mekhq.campaign.finances.Money; import mekhq.campaign.personnel.Person; @@ -77,7 +81,7 @@ public class AtBConfiguration { private HashMap>> botLanceTables = new HashMap<>(); /* Contract generation */ - private ArrayList> hiringHalls; + private HashMap hiringHalls = new HashMap<>(); /* Personnel and unit markets */ private Money shipSearchCost; @@ -93,7 +97,6 @@ public class AtBConfiguration { MekHQ.getMHQOptions().getLocale()); private AtBConfiguration() { - hiringHalls = new ArrayList<>(); dsTable = new WeightedTable<>(); jsTable = new WeightedTable<>(); shipSearchCost = Money.of(100000); @@ -166,10 +169,16 @@ private void setAllValuesToDefaults() { case "hiringHalls": for (String entry : property.split("\\|")) { String[] fields = entry.split(","); - hiringHalls.add(new DatedRecord<>( - !fields[0].isBlank() ? MHQXMLUtility.parseDate(fields[0]) : null, - !fields[1].isBlank() ? MHQXMLUtility.parseDate(fields[1]) : null, - fields[2])); + LocalDate startDate = !fields[0].isBlank() ? MHQXMLUtility.parseDate(fields[0]) : null; + LocalDate endDate = !fields[1].isBlank() ? MHQXMLUtility.parseDate(fields[1]) : null; + HiringHallLevel level = null; + try { + level = HiringHallLevel.valueOf(fields[2].toUpperCase()); + } catch (IllegalArgumentException ex) { + level = HiringHallLevel.GREAT; + } + String name = fields[3]; + hiringHalls.put(name, new HiringHall(level, startDate, endDate, name)); } break; case "shipSearchCost": @@ -315,8 +324,16 @@ public static String getParentFactionType(final Faction faction) { } public boolean isHiringHall(String planet, LocalDate date) { - return hiringHalls.stream().anyMatch(rec -> rec.getValue().equals(planet) - && rec.fitsDate(date)); + HiringHall hall = hiringHalls.get(planet); + return hall != null && hall.isActive(date); + } + + public HiringHallLevel getHiringHallLevel(String planet, LocalDate date) { + HiringHall hall = hiringHalls.get(planet); + if (hall != null && hall.isActive(date)) { + return hall.getLevel(); + } + return HiringHallLevel.NONE; } public Money getShipSearchCost() { @@ -499,7 +516,19 @@ private void loadContractGenerationNodeFromXml(Node node) { if (wn2.getAttributes().getNamedItem("end") != null) { end = MHQXMLUtility.parseDate(wn2.getAttributes().getNamedItem("end").getTextContent()); } - hiringHalls.add(new DatedRecord<>(start, end, wn2.getTextContent())); + HiringHallLevel level = HiringHallLevel.NONE; + if (wn2.getAttributes().getNamedItem("level") != null) { + try { + level = HiringHallLevel.valueOf(wn2.getAttributes().getNamedItem("level").getTextContent().toUpperCase()); + } catch (IllegalArgumentException e) { + logger.warn("Invalid value for Hiring Hall level, falling back to NONE: " + e); + } + } else { + // Backwards compatibility--hiring halls in atbconfig.xml should default to GREAT + level = HiringHallLevel.GREAT; + } + String planetName = wn2.getTextContent(); + hiringHalls.put(planetName, new HiringHall(level, start, end, planetName)); } } } diff --git a/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java b/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java index 0bbc8daf25..db0f7cb6a0 100644 --- a/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java +++ b/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java @@ -37,7 +37,8 @@ import mekhq.campaign.force.Force; import mekhq.campaign.force.Lance; import mekhq.campaign.icons.UnitIcon; -import mekhq.campaign.market.ContractMarket; +import mekhq.campaign.market.contractMarket.AbstractContractMarket; +import mekhq.campaign.market.contractMarket.AtbMonthlyContractMarket; import mekhq.campaign.market.PersonnelMarket; import mekhq.campaign.market.ShoppingList; import mekhq.campaign.mission.AtBContract; @@ -271,7 +272,7 @@ public Campaign parse() throws CampaignXmlParseException, NullEntityException { foundPersonnelMarket = true; } else if (xn.equalsIgnoreCase("contractMarket")) { // CAW: implicit DEPENDS-ON to the node - retVal.setContractMarket(ContractMarket.generateInstanceFromXML(wn, retVal, version)); + retVal.setContractMarket(AbstractContractMarket.generateInstanceFromXML(wn, retVal, version)); foundContractMarket = true; } else if (xn.equalsIgnoreCase("unitMarket")) { // Windchild: implicit DEPENDS ON to the nodes @@ -486,7 +487,7 @@ public Campaign parse() throws CampaignXmlParseException, NullEntityException { } if (!foundContractMarket) { - retVal.setContractMarket(new ContractMarket()); + retVal.setContractMarket(new AtbMonthlyContractMarket()); } if (!foundUnitMarket) { diff --git a/MekHQ/src/mekhq/campaign/market/contractMarket/AbstractContractMarket.java b/MekHQ/src/mekhq/campaign/market/contractMarket/AbstractContractMarket.java new file mode 100644 index 0000000000..80c4ab09a1 --- /dev/null +++ b/MekHQ/src/mekhq/campaign/market/contractMarket/AbstractContractMarket.java @@ -0,0 +1,538 @@ +package mekhq.campaign.market.contractMarket; + +import megamek.Version; +import megamek.codeUtilities.MathUtility; +import megamek.common.Compute; +import megamek.common.enums.SkillLevel; +import megamek.logging.MMLogger; +import mekhq.campaign.Campaign; +import mekhq.campaign.market.enums.ContractMarketMethod; +import mekhq.campaign.mission.AtBContract; +import mekhq.campaign.mission.Contract; +import mekhq.campaign.mission.Mission; +import mekhq.campaign.mission.enums.AtBContractType; +import mekhq.campaign.mission.enums.ContractCommandRights; +import mekhq.campaign.rating.IUnitRating; +import mekhq.campaign.universe.Factions; +import mekhq.campaign.universe.RandomFactionGenerator; +import mekhq.utilities.MHQXMLUtility; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.io.PrintWriter; +import java.util.*; + +/** + * Abstract base class for various Contract Market types in AtB/Stratcon. Responsible for generation + * and initialization of AtBContracts. + */ +public abstract class AbstractContractMarket { + public static final int CLAUSE_COMMAND = 0; + public static final int CLAUSE_SALVAGE = 1; + public static final int CLAUSE_SUPPORT = 2; + public static final int CLAUSE_TRANSPORT = 3; + public static final int CLAUSE_NUM = 4; + + + protected List contracts = new ArrayList<>(); + protected int lastId = 0; + protected Map contractIds = new HashMap<>(); + protected Map clauseMods = new HashMap<>(); + + /** + * An arbitrary maximum number of attempts to generate a contract. + */ + protected final static int MAXIMUM_GENERATION_RETRIES = 3; + + /* It is possible to call addFollowup more than once for the + * same contract by canceling the dialog and running it again; + * this is the easiest place to track it to prevent + * multiple followup contracts. + * key: followup id + * value: main contract id + */ + protected HashMap followupContracts = new HashMap<>(); + + /** + * An arbitrary maximum number of attempts to find a random employer faction that + * is not a Mercenary. + */ + protected final static int MAXIMUM_ATTEMPTS_TO_FIND_NON_MERC_EMPLOYER = 20; + + private final ContractMarketMethod method; + private static final MMLogger logger = MMLogger.create(AbstractContractMarket.class); + + + /** + * Generate a new contract and add it to the market. + * @param campaign + * @return The newly generated contract + */ + abstract public AtBContract addAtBContract(Campaign campaign); + + /** + * Generate available contract offers for the player's force. + * @param campaign + * @param newCampaign Boolean indicating whether this is a fresh campaign. + */ + abstract public void generateContractOffers(Campaign campaign, boolean newCampaign); + + /** + * Add a followup contract to an existing contract. + * @param campaign + * @param contract + */ + abstract public void addFollowup(Campaign campaign, AtBContract contract); + + /** + * Calculate the total payment modifier for the contract based on the configured market method + * (e.g., CAM_OPS, ATB_MONTHLY). + * @param campaign + * @param contract + * @return a double representing the total payment multiplier. + */ + abstract public double calculatePaymentMultiplier(Campaign campaign, AtBContract contract); + + protected AbstractContractMarket(final ContractMarketMethod method) { + this.method = method; + } + + /** + * + * @return the Method (e.g., CAM_OPS, ATB_MONTHLY) associated with the Contract Market instance + */ + public ContractMarketMethod getMethod() { + return method; + } + + /** + * Empty an available contract from the market. + * @param c contract to remove + */ + public void removeContract(Contract c) { + contracts.remove(c); + contractIds.remove(c.getId()); + clauseMods.remove(c.getId()); + followupContracts.remove(c.getId()); + } + + /** + * Rerolls a specific clause in a contract, usually via negotiation. + * @param c the contract being negotiated + * @param clause ID representing the type of clause. + * @param campaign + */ + public void rerollClause(AtBContract c, int clause, Campaign campaign) { + if (null != clauseMods.get(c.getId())) { + switch (clause) { + case CLAUSE_COMMAND -> rollCommandClause(c, clauseMods.get(c.getId()).mods[clause]); + case CLAUSE_SALVAGE -> + rollSalvageClause(c, clauseMods.get(c.getId()).mods[clause], campaign.getCampaignOptions().getContractMaxSalvagePercentage()); + case CLAUSE_TRANSPORT -> rollTransportClause(c, clauseMods.get(c.getId()).mods[clause]); + case CLAUSE_SUPPORT -> rollSupportClause(c, clauseMods.get(c.getId()).mods[clause]); + } + clauseMods.get(c.getId()).rerollsUsed[clause]++; + c.calculateContract(campaign); + } + } + + /** + * Returns the number of rerolls used so far for a specific clause. + * @param c + * @param clause ID representing the type of clause. + * @return + */ + public int getRerollsUsed(Contract c, int clause) { + if (null != clauseMods.get(c.getId())) { + return clauseMods.get(c.getId()).rerollsUsed[clause]; + } + return 0; + } + + /** + * @return a list of currently active contracts on the market + */ + public List getContracts() { + return contracts; + } + + /** + * Empties the market and generates a new batch of contract offers for an existing campaign. + * @param campaign + */ + public void generateContractOffers(Campaign campaign) { + generateContractOffers(campaign, false); + } + + protected void updateReport(Campaign campaign) { + if (campaign.getCampaignOptions().isContractMarketReportRefresh()) { + campaign.addReport("Contract market updated"); + } + } + + /** + * Determines the number of required lances to be deployed for a contract. For Mercenary subcontracts + * this defaults to 1; otherwise the number is based on the number of combat units in the campaign. + * @param campaign + * @param contract + * @return The number of lances required to be deployed. + */ + public int calculateRequiredLances(Campaign campaign, AtBContract contract) { + int maxDeployedLances = calculateMaxDeployedLances(campaign); + if (contract.isSubcontract()) { + return 1; + } else { + int requiredLances = Math.max(AtBContract.getEffectiveNumUnits(campaign) / 6, 1); + return Math.min(requiredLances, maxDeployedLances); + } + } + + /** + * Determine the maximum number of lances the force can deploy. The result is based on the + * commander's Strategy skill and various campaign options. + * @param campaign + * @return the maximum number of lances that can be deployed on the contract. + */ + public int calculateMaxDeployedLances(Campaign campaign) { + return campaign.getCampaignOptions().getBaseStrategyDeployment() + + campaign.getCampaignOptions().getAdditionalStrategyDeployment() * + campaign.getCommanderStrategy(); + } + + protected SkillLevel getSkillRating(int roll) { + if (roll <= 5) { + return SkillLevel.GREEN; + } else if (roll <= 9) { + return SkillLevel.REGULAR; + } else if (roll <= 11) { + return SkillLevel.VETERAN; + } else { + return SkillLevel.ELITE; + } + } + + protected int getQualityRating(int roll) { + if (roll <= 5) { + return IUnitRating.DRAGOON_F; + } else if (roll <= 8) { + return IUnitRating.DRAGOON_D; + } else if (roll <= 10) { + return IUnitRating.DRAGOON_C; + } else if (roll == 11) { + return IUnitRating.DRAGOON_B; + } else { + return IUnitRating.DRAGOON_A; + } + } + + protected void rollCommandClause(final Contract contract, final int modifier) { + final int roll = Compute.d6(2) + modifier; + if (roll < 3) { + contract.setCommandRights(ContractCommandRights.INTEGRATED); + } else if (roll < 8) { + contract.setCommandRights(ContractCommandRights.HOUSE); + } else if (roll < 12) { + contract.setCommandRights(ContractCommandRights.LIAISON); + } else { + contract.setCommandRights(ContractCommandRights.INDEPENDENT); + } + } + + protected void rollSalvageClause(AtBContract contract, int mod, int contractMaxSalvagePercentage) { + contract.setSalvageExchange(false); + int roll = Math.min(Compute.d6(2) + mod, 13); + if (roll < 2) { + contract.setSalvagePct(0); + } else if (roll < 4) { + contract.setSalvageExchange(true); + int r; + do { + r = Compute.d6(2); + } while (r < 4); + contract.setSalvagePct(Math.min((r - 3) * 10, contractMaxSalvagePercentage)); + } else { + contract.setSalvagePct(Math.min((roll - 3) * 10, contractMaxSalvagePercentage)); + } + } + + protected void rollSupportClause(AtBContract contract, int mod) { + int roll = Compute.d6(2) + mod; + contract.setStraightSupport(0); + contract.setBattleLossComp(0); + if (roll < 3) { + contract.setStraightSupport(0); + } else if (roll < 8) { + contract.setStraightSupport((roll - 2) * 20); + } else if (roll == 8) { + contract.setBattleLossComp(10); + } else { + contract.setBattleLossComp(Math.min((roll - 8) * 20, 100)); + } + } + + protected void rollTransportClause(AtBContract contract, int mod) { + int roll = Compute.d6(2) + mod; + if (roll < 2) { + contract.setTransportComp(0); + } else if (roll < 6) { + contract.setTransportComp((20 + (roll - 2) * 5)); + } else if (roll < 10) { + contract.setTransportComp((45 + (roll - 6) * 5)); + } else { + contract.setTransportComp(100); + } + } + + protected AtBContractType findMissionType(int unitRatingMod, boolean majorPower) { + final AtBContractType[][] table = { + // col 0: IS Houses + { AtBContractType.GUERRILLA_WARFARE, AtBContractType.RECON_RAID, AtBContractType.PIRATE_HUNTING, + AtBContractType.PLANETARY_ASSAULT, AtBContractType.OBJECTIVE_RAID, + AtBContractType.OBJECTIVE_RAID, + AtBContractType.EXTRACTION_RAID, AtBContractType.RECON_RAID, AtBContractType.GARRISON_DUTY, + AtBContractType.CADRE_DUTY, AtBContractType.RELIEF_DUTY }, + // col 1: Others + { AtBContractType.GUERRILLA_WARFARE, AtBContractType.RECON_RAID, AtBContractType.PLANETARY_ASSAULT, + AtBContractType.OBJECTIVE_RAID, AtBContractType.EXTRACTION_RAID, AtBContractType.PIRATE_HUNTING, + AtBContractType.SECURITY_DUTY, AtBContractType.OBJECTIVE_RAID, AtBContractType.GARRISON_DUTY, + AtBContractType.CADRE_DUTY, AtBContractType.DIVERSIONARY_RAID } + }; + int roll = MathUtility.clamp(Compute.d6(2) + unitRatingMod - IUnitRating.DRAGOON_C, 2, 12); + return table[majorPower ? 0 : 1][roll - 2]; + } + + protected void setEnemyCode(AtBContract contract) { + if (contract.getContractType().isPirateHunting()) { + contract.setEnemyCode("PIR"); + } else if (contract.getContractType().isRiotDuty()) { + contract.setEnemyCode("REB"); + } else { + contract.setEnemyCode(RandomFactionGenerator.getInstance().getEnemy(contract.getEmployerCode(), + contract.getContractType().isGarrisonType())); + } + } + + protected void setAttacker(AtBContract contract) { + boolean isAttacker = !contract.getContractType().isGarrisonType() + || (contract.getContractType().isReliefDuty() && (Compute.d6() < 4)) + || contract.getEnemy().isRebel(); + contract.setAttacker(isAttacker); + } + + protected void setSystemId(AtBContract contract) throws NoContractLocationFoundException { + // FIXME : Windchild : I don't work properly + if (contract.isAttacker()) { + contract.setSystemId(RandomFactionGenerator.getInstance().getMissionTarget(contract.getEmployerCode(), + contract.getEnemyCode())); + } else { + contract.setSystemId(RandomFactionGenerator.getInstance().getMissionTarget(contract.getEnemyCode(), + contract.getEmployerCode())); + } + if (contract.getSystem() == null) { + String errorMsg = "Could not find contract location for " + + contract.getEmployerCode() + " vs. " + contract.getEnemyCode(); + logger.warn(errorMsg); + throw new NoContractLocationFoundException(errorMsg); + } + } + + protected void setIsRiotDuty(AtBContract contract) { + if (contract.getContractType().isGarrisonDuty() && contract.getEnemy().isRebel()) { + contract.setContractType(AtBContractType.RIOT_DUTY); + } + } + + protected void setAllyRating(AtBContract contract, int year) { + int mod = 0; + if (contract.getEnemy().isRebelOrPirate()) { + mod -= 1; + } + + if (contract.getContractType().isGuerrillaWarfare() || contract.getContractType().isCadreDuty()) { + mod -= 3; + } else if (contract.getContractType().isGarrisonDuty() || contract.getContractType().isSecurityDuty()) { + mod -= 2; + } + + if (AtBContract.isMinorPower(contract.getEmployerCode())) { + mod -= 1; + } + + if (contract.getEnemy().isIndependent()) { + mod -= 2; + } + + if (contract.getContractType().isPlanetaryAssault()) { + mod += 1; + } + + if (Factions.getInstance().getFaction(contract.getEmployerCode()).isClan() && !contract.isAttacker()) { + // facing front-line units + mod += 1; + } + contract.setAllySkill(getSkillRating(Compute.d6(2) + mod)); + if (year > 2950 && year < 3039 && + !Factions.getInstance().getFaction(contract.getEmployerCode()).isClan()) { + mod -= 1; + } + contract.setAllyQuality(getQualityRating(Compute.d6(2) + mod)); + } + + protected void setEnemyRating(AtBContract contract, int year) { + int mod = 0; + if (contract.getEnemy().isRebelOrPirate()) { + mod -= 2; + } + if (contract.getContractType().isGuerrillaWarfare()) { + mod += 2; + } + if (contract.getContractType().isPlanetaryAssault()) { + mod += 1; + } + if (AtBContract.isMinorPower(contract.getEmployerCode())) { + mod -= 1; + } + if (Factions.getInstance().getFaction(contract.getEmployerCode()).isClan()) { + mod += contract.isAttacker() ? 2 : 4; + } + contract.setEnemySkill(getSkillRating(Compute.d6(2) + mod)); + if (year > 2950 && year < 3039 && + !Factions.getInstance().getFaction(contract.getEnemyCode()).isClan()) { + mod -= 1; + } + contract.setEnemyQuality(getQualityRating(Compute.d6(2) + mod)); + } + + public void writeToXML(final PrintWriter pw, int indent) { + MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, "contractMarket"); + MHQXMLUtility.writeSimpleXMLTag(pw, indent, "method", method.toString()); + MHQXMLUtility.writeSimpleXMLTag(pw, indent, "lastId", lastId); + for (final Contract contract : contracts) { + contract.writeToXML(pw, indent); + } + + for (final Integer key : clauseMods.keySet()) { + if (!contractIds.containsKey(key)) { + continue; + } + + MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, "clauseMods", "id", key); + MHQXMLUtility.writeSimpleXMLTag(pw, indent, "mods", clauseMods.get(key).mods); + MHQXMLUtility.writeSimpleXMLTag(pw, indent, "rerollsUsed", clauseMods.get(key).rerollsUsed); + MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, "clauseMods"); + } + MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, "contractMarket"); + } + + public static AbstractContractMarket generateInstanceFromXML(Node wn, Campaign c, Version version) { + AbstractContractMarket retVal = null; + + try { + retVal = parseMarketMethod(wn); + // Okay, now load Part-specific fields! + NodeList nl = wn.getChildNodes(); + + // Loop through the nodes and load our contract offers + for (int x = 0; x < nl.getLength(); x++) { + Node wn2 = nl.item(x); + + // If it's not an element node, we ignore it. + if (wn2.getNodeType() != Node.ELEMENT_NODE) { + continue; + } + if (wn2.getNodeName().equalsIgnoreCase("lastId")) { + retVal.lastId = Integer.parseInt(wn2.getTextContent()); + } else if (wn2.getNodeName().equalsIgnoreCase("mission")) { + Mission m = Mission.generateInstanceFromXML(wn2, c, version); + + if (m instanceof Contract) { + retVal.contracts.add((Contract) m); + retVal.contractIds.put(m.getId(), (Contract) m); + } + } else if (wn2.getNodeName().equalsIgnoreCase("clauseMods")) { + int key = Integer.parseInt(wn2.getAttributes().getNamedItem("id").getTextContent()); + ClauseMods cm = new ClauseMods(); + NodeList nl2 = wn2.getChildNodes(); + for (int i = 0; i < nl2.getLength(); i++) { + Node wn3 = nl2.item(i); + if (wn3.getNodeName().equalsIgnoreCase("mods")) { + String [] s = wn3.getTextContent().split(","); + for (int j = 0; j < s.length; j++) { + cm.mods[j] = Integer.parseInt(s[j]); + } + } else if (wn3.getNodeName().equalsIgnoreCase("rerollsUsed")) { + String [] s = wn3.getTextContent().split(","); + for (int j = 0; j < s.length; j++) { + cm.rerollsUsed[j] = Integer.parseInt(s[j]); + } + } + } + retVal.clauseMods.put(key, cm); + } + } + + // Restore any parent contract references + for (Contract contract : retVal.contracts) { + if (contract instanceof AtBContract) { + final AtBContract atbContract = (AtBContract) contract; + atbContract.restore(c); + } + } + } catch (Exception ex) { + // Errrr, apparently either the class name was invalid... + // Or the listed name doesn't exist. + // Doh! + logger.error("", ex); + } + + return retVal; + } + + private static AbstractContractMarket parseMarketMethod(Node xmlNode) { + AbstractContractMarket market = null; + NodeList nodeList = xmlNode.getChildNodes(); + for (int x = 0; x < nodeList.getLength(); x++) { + Node childNode = nodeList.item(x); + if (childNode.getNodeType() != Node.ELEMENT_NODE) { + continue; + } + if (childNode.getNodeName().equalsIgnoreCase("method")) { + String name = childNode.getTextContent(); + if (Objects.equals(name, ContractMarketMethod.CAM_OPS.toString())) { + market = new CamOpsContractMarket(); + break; + } else if (Objects.equals(name, ContractMarketMethod.NONE.toString())) { + market = new DisabledContractMarket(); + break; + } else { + market = new AtbMonthlyContractMarket(); + break; + } + } + } + if (market == null) { + logger.warn("No Contract Market method found in XML...falling back to AtB_Monthly"); + market = new AtbMonthlyContractMarket(); + } + return market; + } + + /* Keep track of how many rerolls remain for each contract clause + * based on the admin's negotiation skill. Also track bonuses, as + * the random clause bonuses should be persistent. + */ + protected static class ClauseMods { + public int[] rerollsUsed = {0, 0, 0, 0}; + public int[] mods = {0, 0, 0, 0}; + } + + /** + * Exception indicating that no valid location was generated for a contract and that the contract + * is invalid. + */ + public static class NoContractLocationFoundException extends RuntimeException { + public NoContractLocationFoundException(String message) { + super(message); + } + } +} diff --git a/MekHQ/src/mekhq/campaign/market/ContractMarket.java b/MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java similarity index 54% rename from MekHQ/src/mekhq/campaign/market/ContractMarket.java rename to MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java index b67c14e312..6e2f237146 100644 --- a/MekHQ/src/mekhq/campaign/market/ContractMarket.java +++ b/MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java @@ -18,21 +18,12 @@ * You should have received a copy of the GNU General Public License * along with MekHQ. If not, see . */ -package mekhq.campaign.market; +package mekhq.campaign.market.contractMarket; -import java.io.PrintWriter; import java.time.format.DateTimeFormatter; import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; import java.util.Set; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import megamek.Version; -import megamek.codeUtilities.MathUtility; import megamek.common.Compute; import megamek.common.annotations.Nullable; import megamek.common.enums.SkillLevel; @@ -42,8 +33,6 @@ import mekhq.campaign.JumpPath; import mekhq.campaign.market.enums.ContractMarketMethod; import mekhq.campaign.mission.AtBContract; -import mekhq.campaign.mission.Contract; -import mekhq.campaign.mission.Mission; import mekhq.campaign.mission.enums.AtBContractType; import mekhq.campaign.mission.enums.ContractCommandRights; import mekhq.campaign.personnel.Person; @@ -55,7 +44,6 @@ import mekhq.campaign.universe.PlanetarySystem; import mekhq.campaign.universe.RandomFactionGenerator; import mekhq.campaign.universe.Systems; -import mekhq.utilities.MHQXMLUtility; /** * Contract offers that are generated monthly under AtB rules. @@ -64,65 +52,14 @@ * * @author Neoancient */ -public class ContractMarket { - private static final MMLogger logger = MMLogger.create(ContractMarket.class); - - // TODO: Implement a method that rolls each day to see whether a new contract - // appears or an offer disappears - - public final static int CLAUSE_COMMAND = 0; - public final static int CLAUSE_SALVAGE = 1; - public final static int CLAUSE_SUPPORT = 2; - public final static int CLAUSE_TRANSPORT = 3; - public final static int CLAUSE_NUM = 4; - - /** - * An arbitrary maximum number of attempts to generate a contract. - */ - private final static int MAXIMUM_GENERATION_RETRIES = 3; - - /** - * An arbitrary maximum number of attempts to find a random employer faction - * that - * is not a Mercenary. - */ - private final static int MAXIMUM_ATTEMPTS_TO_FIND_NON_MERC_EMPLOYER = 20; - - private ContractMarketMethod method = ContractMarketMethod.ATB_MONTHLY; - - private List contracts; - private int lastId = 0; - private Map contractIds; - private Map clauseMods; - - /* - * It is possible to call addFollowup more than once for the - * same contract by canceling the dialog and running it again; - * this is the easiest place to track it to prevent - * multiple followup contracts. - * key: followup id - * value: main contract id - */ - private HashMap followupContracts; - - public ContractMarket() { - contracts = new ArrayList<>(); - contractIds = new HashMap<>(); - clauseMods = new HashMap<>(); - followupContracts = new HashMap<>(); - } - - public List getContracts() { - return contracts; - } +public class AtbMonthlyContractMarket extends AbstractContractMarket { + private static final MMLogger logger = MMLogger.create(AtbMonthlyContractMarket.class); - public void removeContract(Contract c) { - contracts.remove(c); - contractIds.remove(c.getId()); - clauseMods.remove(c.getId()); - followupContracts.remove(c.getId()); + public AtbMonthlyContractMarket() { + super(ContractMarketMethod.ATB_MONTHLY); } + @Override public AtBContract addAtBContract(Campaign campaign) { AtBContract c = generateAtBContract(campaign, campaign.getAtBUnitRatingMod()); if (c != null) { @@ -131,42 +68,9 @@ public AtBContract addAtBContract(Campaign campaign) { return c; } - public int getRerollsUsed(Contract c, int clause) { - if (null != clauseMods.get(c.getId())) { - return clauseMods.get(c.getId()).rerollsUsed[clause]; - } - return 0; - } - - public void rerollClause(AtBContract c, int clause, Campaign campaign) { - if (null != clauseMods.get(c.getId())) { - switch (clause) { - case CLAUSE_COMMAND: - rollCommandClause(c, clauseMods.get(c.getId()).mods[clause]); - break; - case CLAUSE_SALVAGE: - rollSalvageClause(c, clauseMods.get(c.getId()).mods[clause], - campaign.getCampaignOptions().getContractMaxSalvagePercentage()); - break; - case CLAUSE_TRANSPORT: - rollTransportClause(c, clauseMods.get(c.getId()).mods[clause]); - break; - case CLAUSE_SUPPORT: - rollSupportClause(c, clauseMods.get(c.getId()).mods[clause]); - break; - } - clauseMods.get(c.getId()).rerollsUsed[clause]++; - c.calculateContract(campaign); - } - } - - public void generateContractOffers(Campaign campaign) { - generateContractOffers(campaign, false); - } - + @Override public void generateContractOffers(Campaign campaign, boolean newCampaign) { - if (((method == ContractMarketMethod.ATB_MONTHLY) && (campaign.getLocalDate().getDayOfMonth() == 1)) - || newCampaign) { + if (((campaign.getLocalDate().getDayOfMonth() == 1)) || newCampaign) { // need to copy to prevent concurrent modification errors new ArrayList<>(contracts).forEach(this::removeContract); @@ -187,8 +91,7 @@ public void generateContractOffers(Campaign campaign, boolean newCampaign) { boolean inBackwater = true; if (currentFactions.size() > 1) { - // More than one faction, if any is *not* periphery, we're not in backwater - // either + // More than one faction, if any is *not* periphery, we're not in backwater either for (Faction f : currentFactions) { if (!f.isPeriphery()) { inBackwater = false; @@ -198,8 +101,7 @@ public void generateContractOffers(Campaign campaign, boolean newCampaign) { // Just one faction. Are there any others nearby? Faction onlyFaction = currentFactions.iterator().next(); if (!onlyFaction.isPeriphery()) { - for (PlanetarySystem key : Systems.getInstance().getNearbySystems(campaign.getCurrentSystem(), - 30)) { + for (PlanetarySystem key : Systems.getInstance().getNearbySystems(campaign.getCurrentSystem(), 30)) { for (Faction f : key.getFactionSet(campaign.getLocalDate())) { if (!onlyFaction.equals(f)) { inBackwater = false; @@ -227,11 +129,8 @@ public void generateContractOffers(Campaign campaign, boolean newCampaign) { if (campaign.getAtBConfig().isHiringHall(campaign.getCurrentSystem().getId(), campaign.getLocalDate())) { numContracts++; - /* - * Though the rules do not state these modifiers are mutually exclusive, the - * fact that the - * distance of Galatea from a border means that it has no advantage for Mercs - * over border + /* Though the rules do not state these modifiers are mutually exclusive, the fact that the + * distance of Galatea from a border means that it has no advantage for Mercs over border * worlds. Common sense dictates that worlds with hiring halls should not be * subject to the -1 for backwater/interior. */ @@ -240,16 +139,12 @@ public void generateContractOffers(Campaign campaign, boolean newCampaign) { } } } else { - /* - * Per IOps Beta, government units determine number of contracts as on a system - * with a great hall - */ + /* Per IOps Beta, government units determine number of contracts as on a system with a great hall */ numContracts++; } /* - * If located on a faction's capital (interpreted as the starting planet for - * that faction), + * If located on a faction's capital (interpreted as the starting planet for that faction), * generate one contract offer for that faction. */ for (Faction f : campaign.getCurrentSystem().getFactionSet(campaign.getLocalDate())) { @@ -277,9 +172,7 @@ public void generateContractOffers(Campaign campaign, boolean newCampaign) { contracts.add(c); } } - if (campaign.getCampaignOptions().isContractMarketReportRefresh()) { - campaign.addReport("Contract market updated"); - } + updateReport(campaign); } } @@ -305,8 +198,7 @@ private void checkForSubcontracts(Campaign campaign, AtBContract contract, int u /* * If no suitable planet can be found or no jump path to the planet can be - * calculated after - * the indicated number of retries, this will return null. + * calculated after the indicated number of retries, this will return null. */ private @Nullable AtBContract generateAtBContract(Campaign campaign, int unitRatingMod) { if (campaign.getFaction().isMercenary()) { @@ -332,8 +224,7 @@ private void checkForSubcontracts(Campaign campaign, AtBContract contract, int u return generateAtBContract(campaign, employer, unitRatingMod, MAXIMUM_GENERATION_RETRIES); } - private @Nullable AtBContract generateAtBContract(Campaign campaign, @Nullable String employer, int unitRatingMod, - int retries) { + private @Nullable AtBContract generateAtBContract(Campaign campaign, @Nullable String employer, int unitRatingMod, int retries) { if (employer == null) { logger.warn("Could not generate an AtB Contract because there was no employer!"); return null; @@ -362,31 +253,19 @@ private void checkForSubcontracts(Campaign campaign, AtBContract contract, int u } } contract.setEmployerCode(employer, campaign.getGameYear()); - contract.setContractType(findAtBMissionType(unitRatingMod, + contract.setContractType(findMissionType(unitRatingMod, Factions.getInstance().getFaction(contract.getEmployerCode()).isISMajorOrSuperPower())); - if (contract.getContractType().isPirateHunting()) { - contract.setEnemyCode("PIR"); - } else if (contract.getContractType().isRiotDuty()) { - contract.setEnemyCode("REB"); - } else { - contract.setEnemyCode(RandomFactionGenerator.getInstance().getEnemy(contract.getEmployerCode(), - contract.getContractType().isGarrisonType())); - } - - if (contract.getContractType().isGarrisonDuty() && contract.getEnemy().isRebel()) { - contract.setContractType(AtBContractType.RIOT_DUTY); - } + setEnemyCode(contract); + setIsRiotDuty(contract); /* * Addition to AtB rules: factions which are generally neutral * (ComStar, Mercs not under contract) are more likely to have garrison-type * contracts and less likely to have battle-type contracts unless at war. */ - if (RandomFactionGenerator.getInstance().getFactionHints() - .isNeutral(Factions.getInstance().getFaction(employer)) && - !RandomFactionGenerator.getInstance().getFactionHints().isAtWarWith( - Factions.getInstance().getFaction(employer), + if (RandomFactionGenerator.getInstance().getFactionHints().isNeutral(Factions.getInstance().getFaction(employer)) && + !RandomFactionGenerator.getInstance().getFactionHints().isAtWarWith(Factions.getInstance().getFaction(employer), Factions.getInstance().getFaction(contract.getEnemyCode()), campaign.getLocalDate())) { if (contract.getContractType().isPlanetaryAssault()) { contract.setContractType(AtBContractType.GARRISON_DUTY); @@ -394,21 +273,10 @@ private void checkForSubcontracts(Campaign campaign, AtBContract contract, int u contract.setContractType(AtBContractType.SECURITY_DUTY); } } - - // FIXME : Windchild : I don't work properly - boolean isAttacker = !contract.getContractType().isGarrisonType() - || (contract.getContractType().isReliefDuty() && (Compute.d6() < 4)) - || contract.getEnemy().isRebel(); - if (isAttacker) { - contract.setSystemId(RandomFactionGenerator.getInstance().getMissionTarget(contract.getEmployerCode(), - contract.getEnemyCode())); - } else { - contract.setSystemId(RandomFactionGenerator.getInstance().getMissionTarget(contract.getEnemyCode(), - contract.getEmployerCode())); - } - if (contract.getSystem() == null) { - logger.warn("Could not find contract location for " - + contract.getEmployerCode() + " vs. " + contract.getEnemyCode()); + setAttacker(contract); + try { + setSystemId(contract); + } catch (NoContractLocationFoundException ex) { return generateAtBContract(campaign, employer, unitRatingMod, retries - 1); } JumpPath jp = null; @@ -424,8 +292,8 @@ private void checkForSubcontracts(Campaign campaign, AtBContract contract, int u return generateAtBContract(campaign, employer, unitRatingMod, retries - 1); } - setAllyRating(contract, isAttacker, campaign.getGameYear()); - setEnemyRating(contract, isAttacker, campaign.getGameYear()); + setAllyRating(contract, campaign.getGameYear()); + setEnemyRating(contract, campaign.getGameYear()); if (contract.getContractType().isCadreDuty()) { contract.setAllySkill(SkillLevel.GREEN); @@ -433,9 +301,10 @@ private void checkForSubcontracts(Campaign campaign, AtBContract contract, int u } contract.calculateLength(campaign.getCampaignOptions().isVariableContractLength()); - setAtBContractClauses(contract, unitRatingMod, campaign); + setContractClauses(contract, unitRatingMod, campaign); - contract.calculatePaymentMultiplier(campaign); + contract.setRequiredLances(calculateRequiredLances(campaign, contract)); + contract.setMultiplier(calculatePaymentMultiplier(campaign, contract)); contract.setPartsAvailabilityLevel(contract.getContractType().calculatePartsAvailabilityLevel()); @@ -444,9 +313,8 @@ private void checkForSubcontracts(Campaign campaign, AtBContract contract, int u contract.setName(String.format("%s - %s - %s %s", contract.getStartDate().format(DateTimeFormatter.ofPattern("yyyy") - .withLocale(MekHQ.getMHQOptions().getDateLocale())), - employer, - contract.getSystem().getName(contract.getStartDate()), contract.getContractType())); + .withLocale(MekHQ.getMHQOptions().getDateLocale())), employer, + contract.getSystem().getName(contract.getStartDate()), contract.getContractType())); return contract; } @@ -455,7 +323,7 @@ protected AtBContract generateAtBSubcontract(Campaign campaign, AtBContract parent, int unitRatingMod) { AtBContract contract = new AtBContract("New Subcontract"); contract.setEmployerCode(parent.getEmployerCode(), campaign.getGameYear()); - contract.setContractType(findAtBMissionType(unitRatingMod, + contract.setContractType(findMissionType(unitRatingMod, Factions.getInstance().getFaction(contract.getEmployerCode()).isISMajorOrSuperPower())); if (contract.getContractType().isPirateHunting()) { @@ -484,8 +352,7 @@ protected AtBContract generateAtBSubcontract(Campaign campaign, */ // TODO : When MekHQ gets the capability of splitting the unit to different - // locations, this - // TODO : restriction can be lessened or lifted. + // locations, this restriction can be lessened or lifted. if (!contract.getEnemy().isRebelOrPirate()) { boolean factionValid = false; for (PlanetarySystem p : Systems.getInstance().getNearbySystems(campaign.getCurrentSystem(), 30)) { @@ -505,13 +372,10 @@ protected AtBContract generateAtBSubcontract(Campaign campaign, } } - // FIXME : Windchild : I don't work properly - boolean isAttacker = !contract.getContractType().isGarrisonType() - || (contract.getContractType().isReliefDuty() && (Compute.d6() < 4)) - || contract.getEnemy().isRebel(); + setAttacker(contract); contract.setSystemId(parent.getSystemId()); - setAllyRating(contract, isAttacker, campaign.getGameYear()); - setEnemyRating(contract, isAttacker, campaign.getGameYear()); + setAllyRating(contract, campaign.getGameYear()); + setEnemyRating(contract, campaign.getGameYear()); if (contract.getContractType().isCadreDuty()) { contract.setAllySkill(SkillLevel.GREEN); @@ -533,19 +397,20 @@ protected AtBContract generateAtBSubcontract(Campaign campaign, } contract.setTransportComp(100); - contract.calculatePaymentMultiplier(campaign); + contract.setRequiredLances(calculateRequiredLances(campaign, contract)); + contract.setMultiplier(calculatePaymentMultiplier(campaign, contract)); contract.setPartsAvailabilityLevel(contract.getContractType().calculatePartsAvailabilityLevel()); contract.calculateContract(campaign); contract.setName(String.format("%s - %s - %s Subcontract %s", contract.getStartDate().format(DateTimeFormatter.ofPattern("yyyy") - .withLocale(MekHQ.getMHQOptions().getDateLocale())), - contract.getEmployer(), + .withLocale(MekHQ.getMHQOptions().getDateLocale())), contract.getEmployer(), contract.getSystem().getName(parent.getStartDate()), contract.getContractType())); return contract; } + @Override public void addFollowup(Campaign campaign, AtBContract contract) { if (followupContracts.containsValue(contract.getId())) { @@ -573,9 +438,10 @@ public void addFollowup(Campaign campaign, followup.setEnemySkill(contract.getEnemySkill()); followup.setEnemyQuality(contract.getEnemyQuality()); followup.calculateLength(campaign.getCampaignOptions().isVariableContractLength()); - setAtBContractClauses(followup, campaign.getAtBUnitRatingMod(), campaign); + setContractClauses(followup, campaign.getAtBUnitRatingMod(), campaign); - followup.calculatePaymentMultiplier(campaign); + contract.setRequiredLances(calculateRequiredLances(campaign, contract)); + contract.setMultiplier(calculatePaymentMultiplier(campaign, contract)); followup.setPartsAvailabilityLevel(followup.getContractType().calculatePartsAvailabilityLevel()); @@ -589,112 +455,51 @@ public void addFollowup(Campaign campaign, followupContracts.put(followup.getId(), contract.getId()); } - protected AtBContractType findAtBMissionType(int unitRatingMod, boolean majorPower) { - final AtBContractType[][] table = { - // col 0: IS Houses - { AtBContractType.GUERRILLA_WARFARE, AtBContractType.RECON_RAID, AtBContractType.PIRATE_HUNTING, - AtBContractType.PLANETARY_ASSAULT, AtBContractType.OBJECTIVE_RAID, - AtBContractType.OBJECTIVE_RAID, - AtBContractType.EXTRACTION_RAID, AtBContractType.RECON_RAID, AtBContractType.GARRISON_DUTY, - AtBContractType.CADRE_DUTY, AtBContractType.RELIEF_DUTY }, - // col 1: Others - { AtBContractType.GUERRILLA_WARFARE, AtBContractType.RECON_RAID, AtBContractType.PLANETARY_ASSAULT, - AtBContractType.OBJECTIVE_RAID, AtBContractType.EXTRACTION_RAID, AtBContractType.PIRATE_HUNTING, - AtBContractType.SECURITY_DUTY, AtBContractType.OBJECTIVE_RAID, AtBContractType.GARRISON_DUTY, - AtBContractType.CADRE_DUTY, AtBContractType.DIVERSIONARY_RAID } - }; - int roll = MathUtility.clamp(Compute.d6(2) + unitRatingMod - IUnitRating.DRAGOON_C, 2, 12); - return table[majorPower ? 0 : 1][roll - 2]; - } - - public void setAllyRating(AtBContract contract, boolean isAttacker, int year) { - int mod = 0; - if (contract.getEnemy().isRebelOrPirate()) { - mod -= 1; + @Override + public double calculatePaymentMultiplier(Campaign campaign, AtBContract contract) { + int unitRatingMod = campaign.getAtBUnitRatingMod(); + double multiplier = 1.0; + // IntOps reputation factor then Dragoons rating + if (campaign.getCampaignOptions().getUnitRatingMethod().isCampaignOperations()) { + multiplier *= campaign.getReputationFactor(); + } else { + if (unitRatingMod >= IUnitRating.DRAGOON_A) { + multiplier *= 2.0; + } else if (unitRatingMod == IUnitRating.DRAGOON_B) { + multiplier *= 1.5; + } else if (unitRatingMod == IUnitRating.DRAGOON_D) { + multiplier *= 0.8; + } else if (unitRatingMod == IUnitRating.DRAGOON_F) { + multiplier *= 0.5; + } } - if (contract.getContractType().isGuerrillaWarfare() || contract.getContractType().isCadreDuty()) { - mod -= 3; - } else if (contract.getContractType().isGarrisonDuty() || contract.getContractType().isSecurityDuty()) { - mod -= 2; - } + multiplier *= contract.getContractType().getPaymentMultiplier(); - if (AtBContract.isMinorPower(contract.getEmployerCode())) { - mod -= 1; + final Faction employer = Factions.getInstance().getFaction(contract.getEmployerCode()); + final Faction enemy = contract.getEnemy(); + if (employer.isISMajorOrSuperPower() || employer.isClan()) { + multiplier *= 1.2; + } else if (enemy.isIndependent()) { + multiplier *= 1.0; + } else { + multiplier *= 1.1; } - if (contract.getEnemy().isIndependent()) { - mod -= 2; + if (enemy.isRebelOrPirate()) { + multiplier *= 1.1; } - if (contract.getContractType().isPlanetaryAssault()) { - mod += 1; + int requiredLances = calculateRequiredLances(campaign, contract); + int maxDeployedLances = calculateMaxDeployedLances(campaign); + if (requiredLances > maxDeployedLances && campaign.getCampaignOptions().isAdjustPaymentForStrategy()) { + multiplier *= (double) maxDeployedLances / (double) requiredLances; } - if (Factions.getInstance().getFaction(contract.getEmployerCode()).isClan() && !isAttacker) { - // facing front-line units - mod += 1; - } - contract.setAllySkill(getSkillRating(Compute.d6(2) + mod)); - if (year > 2950 && year < 3039 && - !Factions.getInstance().getFaction(contract.getEmployerCode()).isClan()) { - mod -= 1; - } - contract.setAllyQuality(getQualityRating(Compute.d6(2) + mod)); + return multiplier; } - public void setEnemyRating(AtBContract contract, boolean isAttacker, int year) { - int mod = 0; - if (contract.getEnemy().isRebelOrPirate()) { - mod -= 2; - } - if (contract.getContractType().isGuerrillaWarfare()) { - mod += 2; - } - if (contract.getContractType().isPlanetaryAssault()) { - mod += 1; - } - if (AtBContract.isMinorPower(contract.getEmployerCode())) { - mod -= 1; - } - if (Factions.getInstance().getFaction(contract.getEmployerCode()).isClan()) { - mod += isAttacker ? 2 : 4; - } - contract.setEnemySkill(getSkillRating(Compute.d6(2) + mod)); - if (year > 2950 && year < 3039 && - !Factions.getInstance().getFaction(contract.getEnemyCode()).isClan()) { - mod -= 1; - } - contract.setEnemyQuality(getQualityRating(Compute.d6(2) + mod)); - } - - protected SkillLevel getSkillRating(int roll) { - if (roll <= 5) { - return SkillLevel.GREEN; - } else if (roll <= 9) { - return SkillLevel.REGULAR; - } else if (roll <= 11) { - return SkillLevel.VETERAN; - } else { - return SkillLevel.ELITE; - } - } - - protected int getQualityRating(int roll) { - if (roll <= 5) { - return IUnitRating.DRAGOON_F; - } else if (roll <= 8) { - return IUnitRating.DRAGOON_D; - } else if (roll <= 10) { - return IUnitRating.DRAGOON_C; - } else if (roll == 11) { - return IUnitRating.DRAGOON_B; - } else { - return IUnitRating.DRAGOON_A; - } - } - - protected void setAtBContractClauses(AtBContract contract, int unitRatingMod, Campaign campaign) { + private void setContractClauses(AtBContract contract, int unitRatingMod, Campaign campaign) { ClauseMods mods = new ClauseMods(); clauseMods.put(contract.getId(), mods); @@ -706,18 +511,12 @@ protected void setAtBContractClauses(AtBContract contract, int unitRatingMod, Ca * the highest admin skill, or higher negotiation if the admin * skills are equal. */ - Person adminCommand = campaign.findBestInRole(PersonnelRole.ADMINISTRATOR_COMMAND, SkillType.S_ADMIN, - SkillType.S_NEG); - Person adminTransport = campaign.findBestInRole(PersonnelRole.ADMINISTRATOR_TRANSPORT, SkillType.S_ADMIN, - SkillType.S_NEG); - Person adminLogistics = campaign.findBestInRole(PersonnelRole.ADMINISTRATOR_LOGISTICS, SkillType.S_ADMIN, - SkillType.S_NEG); - int adminCommandExp = (adminCommand == null) ? SkillType.EXP_ULTRA_GREEN - : adminCommand.getSkill(SkillType.S_ADMIN).getExperienceLevel(); - int adminTransportExp = (adminTransport == null) ? SkillType.EXP_ULTRA_GREEN - : adminTransport.getSkill(SkillType.S_ADMIN).getExperienceLevel(); - int adminLogisticsExp = (adminLogistics == null) ? SkillType.EXP_ULTRA_GREEN - : adminLogistics.getSkill(SkillType.S_ADMIN).getExperienceLevel(); + Person adminCommand = campaign.findBestInRole(PersonnelRole.ADMINISTRATOR_COMMAND, SkillType.S_ADMIN, SkillType.S_NEG); + Person adminTransport = campaign.findBestInRole(PersonnelRole.ADMINISTRATOR_TRANSPORT, SkillType.S_ADMIN, SkillType.S_NEG); + Person adminLogistics = campaign.findBestInRole(PersonnelRole.ADMINISTRATOR_LOGISTICS, SkillType.S_ADMIN, SkillType.S_NEG); + int adminCommandExp = (adminCommand == null) ? SkillType.EXP_ULTRA_GREEN : adminCommand.getSkill(SkillType.S_ADMIN).getExperienceLevel(); + int adminTransportExp = (adminTransport == null) ? SkillType.EXP_ULTRA_GREEN : adminTransport.getSkill(SkillType.S_ADMIN).getExperienceLevel(); + int adminLogisticsExp = (adminLogistics == null) ? SkillType.EXP_ULTRA_GREEN : adminLogistics.getSkill(SkillType.S_ADMIN).getExperienceLevel(); /* Treat government units like merc units that have a retainer contract */ if ((!campaign.getFaction().isMercenary() && !campaign.getFaction().isPirate()) @@ -811,158 +610,4 @@ protected void setAtBContractClauses(AtBContract contract, int unitRatingMod, Ca rollSupportClause(contract, mods.mods[CLAUSE_SUPPORT]); rollTransportClause(contract, mods.mods[CLAUSE_TRANSPORT]); } - - private void rollCommandClause(final Contract contract, final int modifier) { - final int roll = Compute.d6(2) + modifier; - if (roll < 3) { - contract.setCommandRights(ContractCommandRights.INTEGRATED); - } else if (roll < 8) { - contract.setCommandRights(ContractCommandRights.HOUSE); - } else if (roll < 12) { - contract.setCommandRights(ContractCommandRights.LIAISON); - } else { - contract.setCommandRights(ContractCommandRights.INDEPENDENT); - } - } - - private void rollSalvageClause(AtBContract contract, int mod, int contractMaxSalvagePercentage) { - contract.setSalvageExchange(false); - int roll = Math.min(Compute.d6(2) + mod, 13); - if (roll < 2) { - contract.setSalvagePct(0); - } else if (roll < 4) { - contract.setSalvageExchange(true); - int r; - do { - r = Compute.d6(2); - } while (r < 4); - contract.setSalvagePct(Math.min((r - 3) * 10, contractMaxSalvagePercentage)); - } else { - contract.setSalvagePct(Math.min((roll - 3) * 10, contractMaxSalvagePercentage)); - } - } - - private void rollSupportClause(AtBContract contract, int mod) { - int roll = Compute.d6(2) + mod; - contract.setStraightSupport(0); - contract.setBattleLossComp(0); - if (roll < 3) { - contract.setStraightSupport(0); - } else if (roll < 8) { - contract.setStraightSupport((roll - 2) * 20); - } else if (roll == 8) { - contract.setBattleLossComp(10); - } else { - contract.setBattleLossComp(Math.min((roll - 8) * 20, 100)); - } - } - - private void rollTransportClause(AtBContract contract, int mod) { - int roll = Compute.d6(2) + mod; - if (roll < 2) { - contract.setTransportComp(0); - } else if (roll < 6) { - contract.setTransportComp((20 + (roll - 2) * 5)); - } else if (roll < 10) { - contract.setTransportComp((45 + (roll - 6) * 5)); - } else { - contract.setTransportComp(100); - } - } - - public void writeToXML(final PrintWriter pw, int indent) { - MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, "contractMarket"); - MHQXMLUtility.writeSimpleXMLTag(pw, indent, "lastId", lastId); - for (final Contract contract : contracts) { - contract.writeToXML(pw, indent); - } - - for (final Integer key : clauseMods.keySet()) { - if (!contractIds.containsKey(key)) { - continue; - } - - MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, "clauseMods", "id", key); - MHQXMLUtility.writeSimpleXMLTag(pw, indent, "mods", clauseMods.get(key).mods); - MHQXMLUtility.writeSimpleXMLTag(pw, indent, "rerollsUsed", clauseMods.get(key).rerollsUsed); - MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, "clauseMods"); - } - MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, "contractMarket"); - } - - public static ContractMarket generateInstanceFromXML(Node wn, Campaign c, Version version) { - ContractMarket retVal = null; - - try { - // Instantiate the correct child class, and call its parsing function. - retVal = new ContractMarket(); - - // Okay, now load Part-specific fields! - NodeList nl = wn.getChildNodes(); - - // Loop through the nodes and load our contract offers - for (int x = 0; x < nl.getLength(); x++) { - Node wn2 = nl.item(x); - - // If it's not an element node, we ignore it. - if (wn2.getNodeType() != Node.ELEMENT_NODE) { - continue; - } - - if (wn2.getNodeName().equalsIgnoreCase("lastId")) { - retVal.lastId = Integer.parseInt(wn2.getTextContent()); - } else if (wn2.getNodeName().equalsIgnoreCase("mission")) { - Mission m = Mission.generateInstanceFromXML(wn2, c, version); - - if (m instanceof Contract) { - retVal.contracts.add((Contract) m); - retVal.contractIds.put(m.getId(), (Contract) m); - } - } else if (wn2.getNodeName().equalsIgnoreCase("clauseMods")) { - int key = Integer.parseInt(wn2.getAttributes().getNamedItem("id").getTextContent()); - ClauseMods cm = new ClauseMods(); - NodeList nl2 = wn2.getChildNodes(); - for (int i = 0; i < nl2.getLength(); i++) { - Node wn3 = nl2.item(i); - if (wn3.getNodeName().equalsIgnoreCase("mods")) { - String[] s = wn3.getTextContent().split(","); - for (int j = 0; j < s.length; j++) { - cm.mods[j] = Integer.parseInt(s[j]); - } - } else if (wn3.getNodeName().equalsIgnoreCase("rerollsUsed")) { - String[] s = wn3.getTextContent().split(","); - for (int j = 0; j < s.length; j++) { - cm.rerollsUsed[j] = Integer.parseInt(s[j]); - } - } - } - retVal.clauseMods.put(key, cm); - } - } - - // Restore any parent contract references - for (Contract contract : retVal.contracts) { - if (contract instanceof AtBContract atbContract) { - atbContract.restore(c); - } - } - } catch (Exception ex) { - // Errrr, apparently either the class name was invalid... - // Or the listed name doesn't exist. - // Doh! - logger.error("", ex); - } - - return retVal; - } - - /* - * Keep track of how many rerolls remain for each contract clause - * based on the admin's negotiation skill. Also track bonuses, as - * the random clause bonuses should be persistent. - */ - public static class ClauseMods { - public int[] rerollsUsed = { 0, 0, 0, 0 }; - public int[] mods = { 0, 0, 0, 0 }; - } } diff --git a/MekHQ/src/mekhq/campaign/market/contractMarket/CamOpsContractMarket.java b/MekHQ/src/mekhq/campaign/market/contractMarket/CamOpsContractMarket.java new file mode 100644 index 0000000000..7a543deb96 --- /dev/null +++ b/MekHQ/src/mekhq/campaign/market/contractMarket/CamOpsContractMarket.java @@ -0,0 +1,312 @@ +package mekhq.campaign.market.contractMarket; + +import megamek.common.Compute; +import megamek.common.enums.SkillLevel; +import megamek.logging.MMLogger; +import mekhq.MekHQ; +import mekhq.campaign.Campaign; +import mekhq.campaign.againstTheBot.AtBConfiguration; +import mekhq.campaign.market.enums.ContractMarketMethod; +import mekhq.campaign.mission.AtBContract; +import mekhq.campaign.mission.enums.AtBContractType; +import mekhq.campaign.personnel.Person; +import mekhq.campaign.personnel.SkillType; +import mekhq.campaign.rating.IUnitRating; +import mekhq.campaign.universe.Faction; +import mekhq.campaign.universe.Faction.Tag; +import mekhq.campaign.universe.Factions; +import mekhq.campaign.universe.enums.HiringHallLevel; + +import java.time.format.DateTimeFormatter; +import java.util.*; + +/** + * Contract Market as described in Campaign Operations, 4th printing. + */ +public class CamOpsContractMarket extends AbstractContractMarket { + private static final MMLogger logger = MMLogger.create(CamOpsContractMarket.class); + private static int BASE_NEGOTIATION_TARGET = 8; + private static int EMPLOYER_NEGOTIATION_SKILL_LEVEL = 5; + + private ContractModifiers contractMods = null; + + public CamOpsContractMarket() { + super(ContractMarketMethod.CAM_OPS); + } + + @Override + public AtBContract addAtBContract(Campaign campaign) { + Optional c = generateContract(campaign); + if (c.isPresent()) { + AtBContract atbContract = c.get(); + contracts.add(atbContract); + return atbContract; + } + return null; + } + + @Override + public void generateContractOffers(Campaign campaign, boolean newCampaign) { + if (!(campaign.getLocalDate().getDayOfMonth() == 1) && !newCampaign) { + return; + } + new ArrayList<>(contracts).forEach(this::removeContract); + // TODO: Allow subcontracts? + //for (AtBContract contract : campaign.getActiveAtBContracts()) { + //checkForSubcontracts(campaign, contract, unitRatingMod); + //} + // TODO: CamopsMarket: allow players to choose negotiators and send them out, removing them + // from other tasks they're doing. For now just use the highest negotiation skill on the force. + int ratingMod = getReputationModifier(campaign); + contractMods = generateContractModifiers(campaign); + int negotiationSkill = findNegotiationSkill(campaign); + int numOffers = getNumberOfOffers( + rollNegotiation(negotiationSkill, ratingMod + contractMods.offersMod) - BASE_NEGOTIATION_TARGET); + + for (int i = 0; i < numOffers; i++) { + addAtBContract(campaign); + } + updateReport(campaign); + } + + @Override + public void addFollowup(Campaign campaign, AtBContract contract) { + //TODO: add logic if we decide followup contracts should be allow in CamOps + } + + @Override + public double calculatePaymentMultiplier(Campaign campaign, AtBContract contract) { + //TODO: add logic from camops 4th printing + return 1.0; + } + + private HiringHallLevel getHiringHallLevel(Campaign campaign) { + AtBConfiguration atbConfig = campaign.getAtBConfig(); + return atbConfig.getHiringHallLevel(campaign.getCurrentSystem() + .getPrimaryPlanet() + .getName(campaign.getLocalDate()), + campaign.getLocalDate()); + } + + private ContractModifiers generateContractModifiers(Campaign campaign) { + if (campaign.getFaction().isMercenary()) { + return new ContractModifiers(getHiringHallLevel(campaign)); + } else if (campaign.getFaction().isRebelOrPirate()) { + return new ContractModifiers(HiringHallLevel.NONE); + } else { + return new ContractModifiers(HiringHallLevel.GREAT); + } + } + + private int findNegotiationSkill(Campaign campaign) { + // TODO: have pirates use investigation skill instead when it is implemented per CamOps + Person negotiator = campaign.findBestAtSkill(SkillType.S_NEG); + if (negotiator == null) { + return 0; + } + return negotiator.getSkillLevel(SkillType.S_NEG); + } + + private int rollNegotiation(int skill, int modifiers) { + return Compute.d6(2) + skill + modifiers; + } + + private int rollOpposedNegotiation(int skill, int modifiers) { + return Compute.d6(2) + skill + modifiers - Compute.d6(2) + EMPLOYER_NEGOTIATION_SKILL_LEVEL; + } + + private int getNumberOfOffers(int margin) { + if (margin < 1) { + return 0; + } else if (margin < 3) { + return 1; + } else if (margin < 6) { + return 2; + } else if (margin < 9) { + return 3; + } else if (margin < 11) { + return 4; + } else if (margin < 13) { + return 5; + } else { + return 6; + } + } + + private Optional generateContract(Campaign campaign) { + AtBContract contract = new AtBContract("UnnamedContract"); + lastId++; + contract.setId(lastId); + contractIds.put(lastId, contract); + Faction employer = determineEmployer(campaign); + contract.setEmployerCode(employer.getShortName(), campaign.getLocalDate()); + if (employer.isMercenary()) { + contract.setMercSubcontract(true); + } + contract.setContractType(determineMission(campaign, employer)); + setEnemyCode(contract); + setIsRiotDuty(contract); + setAttacker(contract); + try { + setSystemId(contract); + } catch (NoContractLocationFoundException ex) { + return Optional.empty(); + } + setAllyRating(contract, campaign.getGameYear()); + setEnemyRating(contract, campaign.getGameYear()); + if (contract.getContractType().isCadreDuty()) { + contract.setAllySkill(SkillLevel.GREEN); + contract.setAllyQuality(IUnitRating.DRAGOON_F); + } + contract.calculateLength(campaign.getCampaignOptions().isVariableContractLength()); + setContractClauses(contract, campaign); + contract.setRequiredLances(calculateRequiredLances(campaign, contract)); + contract.setMultiplier(calculatePaymentMultiplier(campaign, contract)); + contract.setPartsAvailabilityLevel(contract.getContractType().calculatePartsAvailabilityLevel()); + contract.initContractDetails(campaign); + contract.calculateContract(campaign); + contract.setName(String.format("%s - %s - %s %s", + contract.getStartDate().format(DateTimeFormatter.ofPattern("yyyy") + .withLocale(MekHQ.getMHQOptions().getDateLocale())), contract.getEmployer(), + contract.getSystem().getName(contract.getStartDate()), contract.getContractType())); + + return Optional.of(contract); + } + + private int getReputationModifier(Campaign campaign) { + return getReputationScore(campaign) / 10; + } + + private int getReputationScore(Campaign campaign) { + return campaign.getReputation().getReputationRating(); + } + + private Faction determineEmployer(Campaign campaign) { + Collection employerTags; + int roll = Compute.d6(2) + getReputationModifier(campaign) + contractMods.employersMod; + if (roll < 6) { + // Roll again on the independent employers column + roll = Compute.d6(2) + getReputationModifier(campaign) + contractMods.employersMod; + employerTags = getEmployerTags(campaign, roll, true); + } else { + employerTags = getEmployerTags(campaign, roll, false); + } + return getRandomEmployer(campaign, employerTags); + } + + private Faction getRandomEmployer(Campaign campaign, Collection employerTags) { + Collection factions = Factions.getInstance().getActiveFactions(campaign.getLocalDate()); + List filtered = new ArrayList<>(); + for (Faction faction : factions) { + // Clans only hire units within their own clan + if (faction.isClan() && !faction.equals(campaign.getFaction())) { + continue; + } + for (Tag employerTag : employerTags) { + if (!faction.is(employerTag)) { + // The SMALL tag has to be converted to independent for now, since for some reason + // independent is coded as a string. + if (employerTag == Tag.SMALL && faction.isIndependent()) { + continue; + } + break; + } + filtered.add(faction); + } + } + Random rand = new Random(); + return filtered.get(rand.nextInt(filtered.size())); + } + + private Collection getEmployerTags(Campaign campaign, int roll, boolean independent) { + Collection tags = new ArrayList<>(); + if (independent) { + tags.add(Tag.SMALL); + if (roll < 4) { + tags.add(Tag.NOBLE); + } else if (roll < 6) { + tags.add(Tag.PLANETARY_GOVERNMENT); + } else if (roll == 6) { + tags.add(Tag.MERC); + } else if (roll < 9) { + tags.add(Tag.PERIPHERY); + tags.add(Tag.MAJOR); + } else if (roll < 11) { + tags.add(Tag.PERIPHERY); + tags.add(Tag.MINOR); + } else { + tags.add(Tag.CORPORATION); + } + } else { + if (roll < 6) { + tags.add(Tag.SMALL); + } else if (roll < 8) { + tags.add(Tag.MINOR); + } else if (roll < 11) { + tags.add(Tag.MAJOR); + } else { + if (Factions.getInstance() + .getActiveFactions(campaign.getLocalDate()) + .stream() + .anyMatch(Faction::isSuperPower)) { + tags.add(Tag.SUPER); + } else { + tags.add(Tag.MAJOR); + } + } + } + return tags; + } + + private AtBContractType determineMission(Campaign campaign, Faction employer) { + int roll = Compute.d6(2); + if (campaign.getFaction().isPirate()) { + if (roll < 6) { + return AtBContractType.RECON_RAID; + } else { + return AtBContractType.OBJECTIVE_RAID; + } + } + return findMissionType(getReputationModifier(campaign), employer.isISMajorOrSuperPower()); + } + + private void setContractClauses(AtBContract contract, Campaign campaign) { + // TODO: add logic to determine initial contract clauses from CamOps 4th printing. + } + + private class ContractModifiers { + protected int offersMod; + protected int employersMod; + protected int missionsMod; + + protected ContractModifiers(HiringHallLevel level) { + switch (level) { + case NONE -> { + offersMod = -3; + employersMod = -2; + missionsMod = -2; + } + case QUESTIONABLE -> { + offersMod = 0; + employersMod = -2; + missionsMod = -2; + } + case MINOR -> { + offersMod = 1; + employersMod = 0; + missionsMod = 0; + } + case STANDARD -> { + offersMod = 2; + employersMod = 1; + missionsMod = 1; + } + case GREAT -> { + offersMod = 3; + employersMod = 2; + missionsMod = 2; + } + } + } + } +} diff --git a/MekHQ/src/mekhq/campaign/market/contractMarket/DisabledContractMarket.java b/MekHQ/src/mekhq/campaign/market/contractMarket/DisabledContractMarket.java new file mode 100644 index 0000000000..3a5f5117bd --- /dev/null +++ b/MekHQ/src/mekhq/campaign/market/contractMarket/DisabledContractMarket.java @@ -0,0 +1,31 @@ +package mekhq.campaign.market.contractMarket; + +import mekhq.campaign.Campaign; +import mekhq.campaign.market.enums.ContractMarketMethod; +import mekhq.campaign.mission.AtBContract; + +public class DisabledContractMarket extends AbstractContractMarket { + public DisabledContractMarket() { + super(ContractMarketMethod.NONE); + } + + @Override + public AtBContract addAtBContract(Campaign campaign) { + return null; + } + + @Override + public void generateContractOffers(Campaign campaign, boolean newCampaign) { + + } + + @Override + public void addFollowup(Campaign campaign, AtBContract contract) { + + } + + @Override + public double calculatePaymentMultiplier(Campaign campaign, AtBContract contract) { + return 1.0; + } +} diff --git a/MekHQ/src/mekhq/campaign/market/enums/ContractMarketMethod.java b/MekHQ/src/mekhq/campaign/market/enums/ContractMarketMethod.java index 03d931c330..b8ae284b81 100644 --- a/MekHQ/src/mekhq/campaign/market/enums/ContractMarketMethod.java +++ b/MekHQ/src/mekhq/campaign/market/enums/ContractMarketMethod.java @@ -19,13 +19,18 @@ package mekhq.campaign.market.enums; import mekhq.MekHQ; +import mekhq.campaign.market.contractMarket.AbstractContractMarket; +import mekhq.campaign.market.contractMarket.AtbMonthlyContractMarket; +import mekhq.campaign.market.contractMarket.CamOpsContractMarket; +import mekhq.campaign.market.contractMarket.DisabledContractMarket; import java.util.ResourceBundle; public enum ContractMarketMethod { //region Enum Declarations NONE("ContractMarketMethod.NONE.text", "ContractMarketMethod.NONE.toolTipText"), - ATB_MONTHLY("ContractMarketMethod.ATB_MONTHLY.text", "ContractMarketMethod.ATB_MONTHLY.toolTipText"); + ATB_MONTHLY("ContractMarketMethod.ATB_MONTHLY.text", "ContractMarketMethod.ATB_MONTHLY.toolTipText"), + CAM_OPS("ContractMarketMethod.CAM_OPS.text", "ContractMarketMethod.CAM_OPS.toolTipText"); //endregion Enum Declarations //region Variable Declarations @@ -56,20 +61,19 @@ public boolean isNone() { public boolean isAtBMonthly() { return this == ATB_MONTHLY; } + + public boolean isCamOps() { + return this == CAM_OPS; + } //endregion Boolean Comparison Methods - // TODO : AbstractContractMarket : Uncomment -/* public AbstractContractMarket getContractMarket() { - switch (this) { - case ATB_MONTHLY: - return new AtBMonthlyContractMarket(); - case NONE: - default: - return new EmptyContractMarket(); - } + return switch (this) { + case ATB_MONTHLY -> new AtbMonthlyContractMarket(); + case CAM_OPS -> new CamOpsContractMarket(); + case NONE -> new DisabledContractMarket(); + }; } -*/ @Override public String toString() { diff --git a/MekHQ/src/mekhq/campaign/mission/AtBContract.java b/MekHQ/src/mekhq/campaign/mission/AtBContract.java index c9d8aa5810..e798589b76 100644 --- a/MekHQ/src/mekhq/campaign/mission/AtBContract.java +++ b/MekHQ/src/mekhq/campaign/mission/AtBContract.java @@ -144,6 +144,7 @@ public class AtBContract extends Contract { protected int nextWeekBattleTypeMod; private StratconCampaignState stratconCampaignState; + private boolean isAttacker; private static final ResourceBundle resources = ResourceBundle.getBundle( "mekhq.resources.AtBContract", @@ -161,6 +162,7 @@ public AtBContract(String name) { parentContract = null; mercSubcontract = false; + isAttacker = false; setContractType(AtBContractType.GARRISON_DUTY); setAllySkill(SkillLevel.REGULAR); @@ -243,62 +245,6 @@ public static boolean isMinorPower(final String factionCode) { return !faction.isMajorOrSuperPower() && !faction.isClan(); } - public void calculatePaymentMultiplier(Campaign campaign) { - int unitRatingMod = campaign.getAtBUnitRatingMod(); - double multiplier = 1.0; - // IntOps reputation factor then Dragoons rating - if (campaign.getCampaignOptions().getUnitRatingMethod().isCampaignOperations()) { - multiplier *= (unitRatingMod * 0.2) + 0.5; - } else { - if (unitRatingMod >= IUnitRating.DRAGOON_A) { - multiplier *= 2.0; - } else if (unitRatingMod == IUnitRating.DRAGOON_B) { - multiplier *= 1.5; - } else if (unitRatingMod == IUnitRating.DRAGOON_D) { - multiplier *= 0.8; - } else if (unitRatingMod == IUnitRating.DRAGOON_F) { - multiplier *= 0.5; - } - } - - multiplier *= getContractType().getPaymentMultiplier(); - - final Faction employer = Factions.getInstance().getFaction(employerCode); - final Faction enemy = getEnemy(); - if (employer.isISMajorOrSuperPower() || employer.isClan()) { - multiplier *= 1.2; - } else if (enemy.isIndependent()) { - multiplier *= 1.0; - } else { - multiplier *= 1.1; - } - - if (enemy.isRebelOrPirate()) { - multiplier *= 1.1; - } - - int cmdrStrategy = 0; - if (campaign.getFlaggedCommander() != null && - campaign.getFlaggedCommander().getSkill(SkillType.S_STRATEGY) != null) { - cmdrStrategy = campaign.getFlaggedCommander().getSkill(SkillType.S_STRATEGY).getLevel(); - } - int maxDeployedLances = campaign.getCampaignOptions().getBaseStrategyDeployment() + - campaign.getCampaignOptions().getAdditionalStrategyDeployment() * - cmdrStrategy; - - if (isSubcontract()) { - requiredLances = 1; - } else { - requiredLances = Math.max(getEffectiveNumUnits(campaign) / 6, 1); - if (requiredLances > maxDeployedLances && campaign.getCampaignOptions().isAdjustPaymentForStrategy()) { - multiplier *= (double) maxDeployedLances / (double) requiredLances; - requiredLances = maxDeployedLances; - } - } - - setMultiplier(multiplier); - } - /** * Checks the morale level of the campaign based on various factors. * @@ -613,6 +559,14 @@ public void setMercSubcontract(boolean sub) { mercSubcontract = sub; } + public boolean isAttacker() { + return isAttacker; + } + + public void setAttacker(boolean isAttacker) { + this.isAttacker = isAttacker; + } + public void checkEvents(Campaign c) { if (c.getLocalDate().getDayOfWeek() == DayOfWeek.MONDAY) { nextWeekBattleTypeMod = 0; diff --git a/MekHQ/src/mekhq/campaign/mission/AtBScenario.java b/MekHQ/src/mekhq/campaign/mission/AtBScenario.java index f893fb639b..438fefed78 100644 --- a/MekHQ/src/mekhq/campaign/mission/AtBScenario.java +++ b/MekHQ/src/mekhq/campaign/mission/AtBScenario.java @@ -1962,10 +1962,16 @@ public void setLance(Lance l) { lanceForceId = l.getForceId(); } + /** + * @return Boolean indicating whether the player is the attacker in this contract. + */ public boolean isAttacker() { return attacker; } + /** + * @param attacker Boolean indicating whether the player is the attacker in this contract. + */ public void setAttacker(boolean attacker) { this.attacker = attacker; } diff --git a/MekHQ/src/mekhq/campaign/universe/Faction.java b/MekHQ/src/mekhq/campaign/universe/Faction.java index 122ac4fa12..8a3b64a044 100644 --- a/MekHQ/src/mekhq/campaign/universe/Faction.java +++ b/MekHQ/src/mekhq/campaign/universe/Faction.java @@ -307,6 +307,22 @@ public boolean isMinorPower() { public boolean isSmall() { return is(Tag.SMALL); } + + public boolean isNoble() { + return is(Tag.NOBLE); + } + + public boolean isPlanetaryGovt() { + return is(Tag.PLANETARY_GOVERNMENT); + } + + public boolean isCorporation() { + return is(Tag.CORPORATION); + } + + public boolean isInactive() { + return is(Tag.INACTIVE); + } // endregion Power Checks // endregion Checks @@ -451,6 +467,12 @@ public enum Tag { /** Faction code is not intended to be for players */ SPECIAL, /** Faction is meant to be played */ - PLAYABLE + PLAYABLE, + /** Faction is an independent noble (Camops p. 39) */ + NOBLE, + /** Faction is an independent planetary government (Camops p. 39) */ + PLANETARY_GOVERNMENT, + /** Faction is an independent corporation (Camops p. 39) */ + CORPORATION } } diff --git a/MekHQ/src/mekhq/campaign/universe/Factions.java b/MekHQ/src/mekhq/campaign/universe/Factions.java index af3d837cbf..4057eafd17 100644 --- a/MekHQ/src/mekhq/campaign/universe/Factions.java +++ b/MekHQ/src/mekhq/campaign/universe/Factions.java @@ -20,6 +20,7 @@ import java.io.FileInputStream; import java.io.IOException; +import java.time.LocalDate; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -31,6 +32,7 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.ParserConfigurationException; +import mekhq.campaign.Campaign; import org.w3c.dom.DOMException; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -97,6 +99,17 @@ public Collection getFactions() { return factions.values(); } + /** + * Returns a list of all factions active in a specific year. + * @param date + * @return + */ + public Collection getActiveFactions(LocalDate date) { + return getFactions().stream().filter(f -> + f.validIn(date) && !f.isInactive()) + .collect(Collectors.toList()); + } + public Collection getFactionList() { return new ArrayList<>(factions.keySet()); } diff --git a/MekHQ/src/mekhq/campaign/universe/HiringHall.java b/MekHQ/src/mekhq/campaign/universe/HiringHall.java new file mode 100644 index 0000000000..c5a22850c3 --- /dev/null +++ b/MekHQ/src/mekhq/campaign/universe/HiringHall.java @@ -0,0 +1,40 @@ +package mekhq.campaign.universe; + +import mekhq.campaign.universe.enums.HiringHallLevel; + +import java.time.LocalDate; + +/** + * Class representing a Hiring Hall on a world on specific dates. This data is statically defined in + * data/universe/atbconfig.xml. + */ +public class HiringHall { + private HiringHallLevel level; + private LocalDate startDate; + private LocalDate endDate; + private String planetName; + + public HiringHall(HiringHallLevel level, LocalDate startDate, LocalDate endDate, String planetName) { + this.level = level; + this.startDate = startDate; + this.endDate = endDate; + this.planetName = planetName; + } + + public boolean isActive(LocalDate date) { + return ((startDate == null) || !startDate.isAfter(date)) + && ((endDate == null) || !endDate.isBefore(date)); + } + + public String getPlanetName() { + return planetName; + } + + public HiringHallLevel getLevel() { + return level; + } + + public void setLevel(HiringHallLevel level) { + this.level = level; + } +} diff --git a/MekHQ/src/mekhq/campaign/universe/enums/HiringHallLevel.java b/MekHQ/src/mekhq/campaign/universe/enums/HiringHallLevel.java new file mode 100644 index 0000000000..5b8ad5704f --- /dev/null +++ b/MekHQ/src/mekhq/campaign/universe/enums/HiringHallLevel.java @@ -0,0 +1,13 @@ +package mekhq.campaign.universe.enums; + +/** + * The level of a Hiring Hall as defined in CamOps (4th printing). Used to determine various modifiers + * related to contract generation. + */ +public enum HiringHallLevel { + NONE, + QUESTIONABLE, + MINOR, + STANDARD, + GREAT +} diff --git a/MekHQ/src/mekhq/gui/CampaignGUI.java b/MekHQ/src/mekhq/gui/CampaignGUI.java index a7d9f3e0c6..29439f17ec 100644 --- a/MekHQ/src/mekhq/gui/CampaignGUI.java +++ b/MekHQ/src/mekhq/gui/CampaignGUI.java @@ -44,6 +44,7 @@ import mekhq.campaign.finances.financialInstitutions.FinancialInstitutions; import mekhq.campaign.force.Force; import mekhq.campaign.icons.StandardForceIcon; +import mekhq.campaign.market.contractMarket.AbstractContractMarket; import mekhq.campaign.market.unitMarket.AbstractUnitMarket; import mekhq.campaign.mission.Scenario; import mekhq.campaign.parts.Part; @@ -1549,6 +1550,11 @@ private void menuOptionsActionPerformed(final ActionEvent evt) { getCampaign().getUnitMarket().setOffers(unitMarket.getOffers()); miUnitMarket.setVisible(!getCampaign().getUnitMarket().getMethod().isNone()); } + + AbstractContractMarket contractMarket = getCampaign().getContractMarket(); + if (contractMarket.getMethod() != newOptions.getContractMarketMethod()) { + getCampaign().setContractMarket(newOptions.getContractMarketMethod().getContractMarket()); + } if (atb != newOptions.isUseAtB()) { if (newOptions.isUseAtB()) { diff --git a/MekHQ/src/mekhq/gui/dialog/ContractMarketDialog.java b/MekHQ/src/mekhq/gui/dialog/ContractMarketDialog.java index 600f999361..016a9c1e0e 100644 --- a/MekHQ/src/mekhq/gui/dialog/ContractMarketDialog.java +++ b/MekHQ/src/mekhq/gui/dialog/ContractMarketDialog.java @@ -46,7 +46,7 @@ import mekhq.campaign.Campaign; import mekhq.campaign.JumpPath; import mekhq.campaign.finances.enums.TransactionType; -import mekhq.campaign.market.ContractMarket; +import mekhq.campaign.market.contractMarket.AbstractContractMarket; import mekhq.campaign.mission.AtBContract; import mekhq.campaign.mission.Contract; import mekhq.campaign.universe.Factions; @@ -72,7 +72,7 @@ public class ContractMarketDialog extends JDialog { private static int sharePct = 20; private Campaign campaign; - private ContractMarket contractMarket; + private AbstractContractMarket contractMarket; private Contract selectedContract = null; private List possibleRetainerContracts; @@ -231,7 +231,7 @@ private void initComponents() { // Changes in rating or force size since creation can alter some details if (c instanceof AtBContract atbContract) { atbContract.initContractDetails(campaign); - atbContract.calculatePaymentMultiplier(campaign); + campaign.getContractMarket().calculatePaymentMultiplier(campaign, atbContract); atbContract.setPartsAvailabilityLevel(atbContract.getContractType().calculatePartsAvailabilityLevel()); atbContract.setAtBSharesPercent(campaign.getCampaignOptions().isUseShareSystem() ? (Integer) spnSharePct.getValue() diff --git a/MekHQ/src/mekhq/gui/dialog/DataLoadingDialog.java b/MekHQ/src/mekhq/gui/dialog/DataLoadingDialog.java index 18be0838b0..090f7a0702 100644 --- a/MekHQ/src/mekhq/gui/dialog/DataLoadingDialog.java +++ b/MekHQ/src/mekhq/gui/dialog/DataLoadingDialog.java @@ -51,6 +51,7 @@ import mekhq.campaign.event.OptionsChangedEvent; import mekhq.campaign.finances.CurrencyManager; import mekhq.campaign.finances.financialInstitutions.FinancialInstitutions; +import mekhq.campaign.market.enums.ContractMarketMethod; import mekhq.campaign.mod.am.InjuryTypes; import mekhq.campaign.personnel.Bloodname; import mekhq.campaign.personnel.SkillType; @@ -229,7 +230,7 @@ public Task(JDialog dialog) { * 5 : Units * 6 : New Campaign / Campaign Loading * 7 : Campaign Application - * + * * @return The loaded campaign * @throws Exception if anything goes wrong */ @@ -346,9 +347,11 @@ public Campaign doInBackground() throws Exception { // Setup Markets campaign.getPersonnelMarket().generatePersonnelForDay(campaign); - // TODO : AbstractContractMarket : Uncomment - // campaign.getContractMarket().generateContractOffers(campaign, (preset == - // null) ? 2 : preset.getContractCount()); + ContractMarketMethod contractMarketMethod = campaign.getCampaignOptions().getContractMarketMethod(); + campaign.setContractMarket(contractMarketMethod.getContractMarket()); + if (!contractMarketMethod.isNone()) { + campaign.getContractMarket().generateContractOffers(campaign, true); + } if (!campaign.getCampaignOptions().getUnitMarketMethod().isNone()) { campaign.setUnitMarket(campaign.getCampaignOptions().getUnitMarketMethod().getUnitMarket()); campaign.getUnitMarket().generateUnitOffers(campaign); diff --git a/MekHQ/src/mekhq/gui/dialog/NewAtBContractDialog.java b/MekHQ/src/mekhq/gui/dialog/NewAtBContractDialog.java index c04aae7273..98e8cfaa87 100644 --- a/MekHQ/src/mekhq/gui/dialog/NewAtBContractDialog.java +++ b/MekHQ/src/mekhq/gui/dialog/NewAtBContractDialog.java @@ -501,8 +501,9 @@ private void updatePlanets() { protected void updatePaymentMultiplier() { if (((AtBContract) contract).getEmployerCode() != null && ((AtBContract) contract).getEnemyCode() != null) { - ((AtBContract) contract).calculatePaymentMultiplier(campaign); - spnMultiplier.setValue(contract.getMultiplier()); + double multiplier = campaign.getContractMarket().calculatePaymentMultiplier(campaign, (AtBContract) contract); + contract.setMultiplier(multiplier); + spnMultiplier.setValue(multiplier); } } diff --git a/MekHQ/src/mekhq/gui/panes/CampaignOptionsPane.java b/MekHQ/src/mekhq/gui/panes/CampaignOptionsPane.java index 4897a0eea2..1923b59cf3 100644 --- a/MekHQ/src/mekhq/gui/panes/CampaignOptionsPane.java +++ b/MekHQ/src/mekhq/gui/panes/CampaignOptionsPane.java @@ -2947,14 +2947,13 @@ private JScrollPane createAgainstTheBotTab() { } // TODO : AbstractContractMarket : Delink more from AtB - if (contractMarketPanel.isEnabled() != enabled) { - comboContractMarketMethod.setSelectedItem(enabled - ? ContractMarketMethod.ATB_MONTHLY - : ContractMarketMethod.NONE); - contractMarketPanel.setEnabled(enabled); - comboContractMarketMethod.setEnabled(false); // TODO : AbstractContractMarket : Remove - // line - } +// if (contractMarketPanel.isEnabled() != enabled) { +// comboContractMarketMethod.setSelectedItem(enabled +// ? ContractMarketMethod.ATB_MONTHLY +// : ContractMarketMethod.NONE); +// contractMarketPanel.setEnabled(enabled); +// comboContractMarketMethod.setEnabled(true); +// } }); GridBagConstraints gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 0; @@ -8114,7 +8113,7 @@ public Component getListCellRendererComponent(final JList list, final Object final boolean cellHasFocus) { super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); if (value instanceof ContractMarketMethod) { - list.setToolTipText(((ContractMarketMethod) value).getToolTipText()); + list.setToolTipText(wordWrap(((ContractMarketMethod) value).getToolTipText())); } return this; } @@ -8131,7 +8130,7 @@ public Component getListCellRendererComponent(final JList list, final Object chkContractMarketReportRefresh.setEnabled(enabled); spnContractMaxSalvagePercentage.setEnabled(enabled); }); - comboContractMarketMethod.setEnabled(false); // TODO : AbstractContractMarket : Remove line + comboContractMarketMethod.setEnabled(true); lblContractSearchRadius.setText(resources.getString("lblContractSearchRadius.text")); lblContractSearchRadius.setToolTipText(resources.getString("lblContractSearchRadius.toolTipText")); diff --git a/MekHQ/src/mekhq/gui/view/ContractSummaryPanel.java b/MekHQ/src/mekhq/gui/view/ContractSummaryPanel.java index a7d1b3617c..4c1fdc30df 100644 --- a/MekHQ/src/mekhq/gui/view/ContractSummaryPanel.java +++ b/MekHQ/src/mekhq/gui/view/ContractSummaryPanel.java @@ -24,7 +24,7 @@ import mekhq.MekHQ; import mekhq.campaign.Campaign; import mekhq.campaign.JumpPath; -import mekhq.campaign.market.ContractMarket; +import mekhq.campaign.market.contractMarket.AbstractContractMarket; import mekhq.campaign.mission.AtBContract; import mekhq.campaign.mission.Contract; import mekhq.campaign.personnel.Person; @@ -319,12 +319,12 @@ public void mouseClicked(MouseEvent e) { } if (contract instanceof AtBContract) { campaign.getContractMarket().rerollClause((AtBContract) contract, - ContractMarket.CLAUSE_COMMAND, campaign); + AbstractContractMarket.CLAUSE_COMMAND, campaign); setCommandRerollButtonText((JButton) ev.getSource()); txtCommand.setText(contract.getCommandRights().toString()); txtCommand.setToolTipText(contract.getCommandRights().getToolTipText()); if (campaign.getContractMarket().getRerollsUsed(contract, - ContractMarket.CLAUSE_COMMAND) >= cmdRerolls) { + AbstractContractMarket.CLAUSE_COMMAND) >= cmdRerolls) { btn.setEnabled(false); } refreshAmounts(); @@ -367,11 +367,11 @@ public void mouseClicked(MouseEvent e) { } if (contract instanceof AtBContract) { campaign.getContractMarket().rerollClause((AtBContract) contract, - ContractMarket.CLAUSE_TRANSPORT, campaign); + AbstractContractMarket.CLAUSE_TRANSPORT, campaign); setTransportRerollButtonText((JButton) ev.getSource()); txtTransport.setText(contract.getTransportComp() + "%"); if (campaign.getContractMarket().getRerollsUsed(contract, - ContractMarket.CLAUSE_TRANSPORT) >= tranRerolls) { + AbstractContractMarket.CLAUSE_TRANSPORT) >= tranRerolls) { btn.setEnabled(false); } refreshAmounts(); @@ -412,12 +412,12 @@ public void mouseClicked(MouseEvent e) { } if (contract instanceof AtBContract) { campaign.getContractMarket().rerollClause((AtBContract) contract, - ContractMarket.CLAUSE_SALVAGE, campaign); + AbstractContractMarket.CLAUSE_SALVAGE, campaign); setSalvageRerollButtonText((JButton) ev.getSource()); txtSalvageRights.setText(contract.getSalvagePct() + "%" + (contract.isSalvageExchange() ? " (Exchange)" : "")); if (campaign.getContractMarket().getRerollsUsed(contract, - ContractMarket.CLAUSE_SALVAGE) >= logRerolls) { + AbstractContractMarket.CLAUSE_SALVAGE) >= logRerolls) { btn.setEnabled(false); } refreshAmounts(); @@ -460,12 +460,12 @@ public void mouseClicked(MouseEvent e) { } if (contract instanceof AtBContract) { campaign.getContractMarket().rerollClause((AtBContract) contract, - ContractMarket.CLAUSE_SUPPORT, campaign); + AbstractContractMarket.CLAUSE_SUPPORT, campaign); setSupportRerollButtonText((JButton) ev.getSource()); txtStraightSupport.setText(contract.getStraightSupport() + "%"); txtBattleLossComp.setText(contract.getBattleLossComp() + "%"); if (campaign.getContractMarket().getRerollsUsed(contract, - ContractMarket.CLAUSE_SUPPORT) >= logRerolls) { + AbstractContractMarket.CLAUSE_SUPPORT) >= logRerolls) { btn.setEnabled(false); } refreshAmounts(); @@ -504,7 +504,7 @@ public void mouseClicked(MouseEvent e) { private boolean hasTransportRerolls() { return allowRerolls && (campaign.getContractMarket().getRerollsUsed(contract, - ContractMarket.CLAUSE_TRANSPORT) < tranRerolls); + AbstractContractMarket.CLAUSE_TRANSPORT) < tranRerolls); } private boolean hasCommandRerolls() { @@ -513,40 +513,40 @@ private boolean hasCommandRerolls() { && (campaign.getFaction().isMercenary() || campaign.getFaction().isPirate()) && (campaign.getContractMarket().getRerollsUsed(contract, - ContractMarket.CLAUSE_COMMAND) < cmdRerolls); + AbstractContractMarket.CLAUSE_COMMAND) < cmdRerolls); } private boolean hasSalvageRerolls() { return allowRerolls && (campaign.getContractMarket().getRerollsUsed(contract, - ContractMarket.CLAUSE_SALVAGE) < logRerolls); + AbstractContractMarket.CLAUSE_SALVAGE) < logRerolls); } private boolean hasSupportRerolls() { return allowRerolls && (campaign.getContractMarket().getRerollsUsed(contract, - ContractMarket.CLAUSE_SUPPORT) < logRerolls); + AbstractContractMarket.CLAUSE_SUPPORT) < logRerolls); } private void setCommandRerollButtonText(JButton rerollButton) { int rerolls = (cmdRerolls - campaign.getContractMarket().getRerollsUsed(contract, - ContractMarket.CLAUSE_COMMAND)); + AbstractContractMarket.CLAUSE_COMMAND)); rerollButton.setText(generateRerollText(rerolls)); } private void setTransportRerollButtonText(JButton rerollButton) { int rerolls = (tranRerolls - campaign.getContractMarket().getRerollsUsed(contract, - ContractMarket.CLAUSE_TRANSPORT)); + AbstractContractMarket.CLAUSE_TRANSPORT)); rerollButton.setText(generateRerollText(rerolls)); } private void setSalvageRerollButtonText(JButton rerollButton) { int rerolls = (logRerolls - campaign.getContractMarket().getRerollsUsed(contract, - ContractMarket.CLAUSE_SALVAGE)); + AbstractContractMarket.CLAUSE_SALVAGE)); rerollButton.setText(generateRerollText(rerolls)); } private void setSupportRerollButtonText(JButton rerollButton) { int rerolls = (logRerolls - campaign.getContractMarket().getRerollsUsed(contract, - ContractMarket.CLAUSE_SUPPORT)); + AbstractContractMarket.CLAUSE_SUPPORT)); rerollButton.setText(generateRerollText(rerolls)); } diff --git a/MekHQ/unittests/mekhq/campaign/market/ContractMarketAtBGenerationTests.java b/MekHQ/unittests/mekhq/campaign/market/ContractMarketAtBGenerationTests.java index 48a010f4a5..4c2583889b 100644 --- a/MekHQ/unittests/mekhq/campaign/market/ContractMarketAtBGenerationTests.java +++ b/MekHQ/unittests/mekhq/campaign/market/ContractMarketAtBGenerationTests.java @@ -22,6 +22,7 @@ import mekhq.campaign.finances.Accountant; import mekhq.campaign.finances.Money; import mekhq.campaign.force.Force; +import mekhq.campaign.market.contractMarket.AtbMonthlyContractMarket; import mekhq.campaign.mission.AtBContract; import mekhq.campaign.rating.IUnitRating; import mekhq.campaign.rating.UnitRatingMethod; @@ -150,7 +151,7 @@ public void addMercWithoutRetainerAtBContractSucceeds(final int gameYear, final doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem)); doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean()); - assertNotNull(new ContractMarket().addAtBContract(campaign)); + assertNotNull(new AtbMonthlyContractMarket().addAtBContract(campaign)); } @ParameterizedTest @@ -252,7 +253,7 @@ public void addMercWithoutRetainerMinorPowerAtBContractSucceeds(final int gameYe doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem)); doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean()); - ContractMarket market = new ContractMarket(); + AtbMonthlyContractMarket market = new AtbMonthlyContractMarket(); AtBContract contract = market.addAtBContract(campaign); assertNotNull(contract); @@ -357,7 +358,7 @@ public void addMercWithoutRetainerEmployerNeutralAtBContractSucceeds(final int g doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem)); doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean()); - ContractMarket market = new ContractMarket(); + AtbMonthlyContractMarket market = new AtbMonthlyContractMarket(); AtBContract contract = market.addAtBContract(campaign); assertNotNull(contract); @@ -463,7 +464,7 @@ public void addMercWithoutRetainerEmployerNeutralAtWarAtBContractSucceeds(final doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem)); doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean()); - ContractMarket market = new ContractMarket(); + AtbMonthlyContractMarket market = new AtbMonthlyContractMarket(); AtBContract contract = market.addAtBContract(campaign); assertNotNull(contract); @@ -572,7 +573,7 @@ public void mercEmployerRetries(final int gameYear, final int unitRating, doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem)); doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean()); - ContractMarket market = new ContractMarket(); + AtbMonthlyContractMarket market = new AtbMonthlyContractMarket(); AtBContract contract = market.addAtBContract(campaign); assertNotNull(contract); @@ -608,7 +609,7 @@ public void mercEmployerRetriesFail(final int gameYear, final int unitRating, // Return "MERC" every time when(rfg.getEmployer()).thenReturn("MERC"); - ContractMarket market = new ContractMarket(); + AtbMonthlyContractMarket market = new AtbMonthlyContractMarket(); AtBContract contract = market.addAtBContract(campaign); assertNull(contract); @@ -713,7 +714,7 @@ public void mercMissiongTargetRetries(final int gameYear, final int unitRating, doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem)); doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean()); - ContractMarket market = new ContractMarket(); + AtbMonthlyContractMarket market = new AtbMonthlyContractMarket(); AtBContract contract = market.addAtBContract(campaign); assertNotNull(contract); @@ -810,7 +811,7 @@ public void mercMissionTargetRetriesFail(final int gameYear, final int unitRatin doReturn(false).when(hints).isNeutral(eq(enemyFaction)); when(rfg.getFactionHints()).thenReturn(hints); - ContractMarket market = new ContractMarket(); + AtbMonthlyContractMarket market = new AtbMonthlyContractMarket(); AtBContract contract = market.addAtBContract(campaign); assertNull(contract); @@ -915,7 +916,7 @@ public void mercJumpPathRetries(final int gameYear, final int unitRating, doReturn(null).doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem)); doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean()); - ContractMarket market = new ContractMarket(); + AtbMonthlyContractMarket market = new AtbMonthlyContractMarket(); AtBContract contract = market.addAtBContract(campaign); assertNotNull(contract); @@ -1014,7 +1015,7 @@ public void mercJumpPathFails(final int gameYear, final int unitRating, // Fail to find a jump path doReturn(null).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem)); - ContractMarket market = new ContractMarket(); + AtbMonthlyContractMarket market = new AtbMonthlyContractMarket(); AtBContract contract = market.addAtBContract(campaign); assertNull(contract); @@ -1118,7 +1119,7 @@ public void addMercWithRetainerAtBContractSucceeds(final int gameYear, final int doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem)); doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean()); - ContractMarket market = new ContractMarket(); + AtbMonthlyContractMarket market = new AtbMonthlyContractMarket(); AtBContract contract = market.addAtBContract(campaign); assertNotNull(contract); @@ -1223,7 +1224,7 @@ public void addMercWithRetainerMinorPowerAtBContractSucceeds(final int gameYear, doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem)); doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean()); - ContractMarket market = new ContractMarket(); + AtbMonthlyContractMarket market = new AtbMonthlyContractMarket(); AtBContract contract = market.addAtBContract(campaign); assertNotNull(contract); @@ -1328,7 +1329,7 @@ public void addMercWithRetainerEmployerNeutralAtBContractSucceeds(final int game doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem)); doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean()); - ContractMarket market = new ContractMarket(); + AtbMonthlyContractMarket market = new AtbMonthlyContractMarket(); AtBContract contract = market.addAtBContract(campaign); assertNotNull(contract); @@ -1433,7 +1434,7 @@ public void addMercWithRetainerEmployerNeutralAtWarAtBContractSucceeds(final int doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem)); doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean()); - ContractMarket market = new ContractMarket(); + AtbMonthlyContractMarket market = new AtbMonthlyContractMarket(); AtBContract contract = market.addAtBContract(campaign); assertNotNull(contract); @@ -1538,7 +1539,7 @@ public void nonMercAtBContractSucceeds(final int gameYear, final int unitRating, doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem)); doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean()); - ContractMarket market = new ContractMarket(); + AtbMonthlyContractMarket market = new AtbMonthlyContractMarket(); AtBContract contract = market.addAtBContract(campaign); assertNotNull(contract); @@ -1643,7 +1644,7 @@ public void nonMercMinorPowerAtBContractSucceeds(final int gameYear, final int u doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem)); doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean()); - ContractMarket market = new ContractMarket(); + AtbMonthlyContractMarket market = new AtbMonthlyContractMarket(); AtBContract contract = market.addAtBContract(campaign); assertNotNull(contract); @@ -1748,7 +1749,7 @@ public void nonMercNeutralAtBContractSucceeds(final int gameYear, final int unit doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem)); doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean()); - ContractMarket market = new ContractMarket(); + AtbMonthlyContractMarket market = new AtbMonthlyContractMarket(); AtBContract contract = market.addAtBContract(campaign); assertNotNull(contract); @@ -1853,7 +1854,7 @@ public void nonMercNeutralAtWarAtBContractSucceeds(final int gameYear, final int doReturn(jumpPath).when(campaign).calculateJumpPath(eq(currentSystem), eq(targetSystem)); doReturn(Money.of(1)).when(campaign).calculateCostPerJump(anyBoolean(), anyBoolean()); - ContractMarket market = new ContractMarket(); + AtbMonthlyContractMarket market = new AtbMonthlyContractMarket(); AtBContract contract = market.addAtBContract(campaign); assertNotNull(contract); diff --git a/MekHQ/unittests/mekhq/campaign/market/ContractMarketIntegrationTest.java b/MekHQ/unittests/mekhq/campaign/market/ContractMarketIntegrationTest.java index 0a91899731..58f934e72c 100644 --- a/MekHQ/unittests/mekhq/campaign/market/ContractMarketIntegrationTest.java +++ b/MekHQ/unittests/mekhq/campaign/market/ContractMarketIntegrationTest.java @@ -22,6 +22,7 @@ import mekhq.campaign.Campaign; import mekhq.campaign.CampaignOptions; import mekhq.campaign.force.Force; +import mekhq.campaign.market.contractMarket.AtbMonthlyContractMarket; import mekhq.campaign.mission.AtBContract; import mekhq.campaign.mission.Contract; import mekhq.campaign.mission.enums.AtBContractType; @@ -84,7 +85,7 @@ public void beforeEach() { @Test public void addAtBContractMercsTest() { - ContractMarket market = new ContractMarket(); + AtbMonthlyContractMarket market = new AtbMonthlyContractMarket(); // Simulate clicking GM Add on the contract market three times for (int ii = 0; ii < REASONABLE_GENERATION_ATTEMPTS; ii++) { @@ -96,7 +97,7 @@ public void addAtBContractMercsTest() { @Test public void generateContractOffersMercsTest() { - ContractMarket market = new ContractMarket(); + AtbMonthlyContractMarket market = new AtbMonthlyContractMarket(); // Simulate three months of contract generation ... boolean foundContract = false; @@ -114,7 +115,7 @@ public void generateContractOffersMercsTest() { public void addAtBContractMercRetainerTest() { campaign.setRetainerEmployerCode("LA"); - ContractMarket market = new ContractMarket(); + AtbMonthlyContractMarket market = new AtbMonthlyContractMarket(); // Simulate clicking GM Add on the contract market three times for (int ii = 0; ii < 3; ii++) { @@ -128,7 +129,7 @@ public void addAtBContractMercRetainerTest() { public void generateContractOffersMercRetainerTest() { campaign.setRetainerEmployerCode("CS"); - ContractMarket market = new ContractMarket(); + AtbMonthlyContractMarket market = new AtbMonthlyContractMarket(); // Simulate three months of contract generation ... boolean foundContract = false; @@ -158,7 +159,7 @@ public void generateContractOffersMercSubcontractTest() { when(existing.getCommandRights()).thenReturn(ContractCommandRights.INDEPENDENT); campaign.importMission(existing); - ContractMarket market = new ContractMarket(); + AtbMonthlyContractMarket market = new AtbMonthlyContractMarket(); SecureRandom realRng = new SecureRandom(); MMRandom rng = mock(MMRandom.class); @@ -205,7 +206,7 @@ public void generateContractOffersMercSubcontractTest() { public void addAtBContractHouseTest() { campaign.setFactionCode("DC"); - ContractMarket market = new ContractMarket(); + AtbMonthlyContractMarket market = new AtbMonthlyContractMarket(); // Simulate clicking GM Add on the contract market three times for (int ii = 0; ii < 3; ii++) { @@ -219,7 +220,7 @@ public void addAtBContractHouseTest() { public void generateContractOffersHouseTest() { campaign.setFactionCode("FS"); - ContractMarket market = new ContractMarket(); + AtbMonthlyContractMarket market = new AtbMonthlyContractMarket(); // Simulate three months of contract generation ... boolean foundContract = false;