diff --git a/MekHQ/data/universe/atbconfig.xml b/MekHQ/data/universe/atbconfig.xml index e278de10ec..6ce1e38f55 100644 --- a/MekHQ/data/universe/atbconfig.xml +++ b/MekHQ/data/universe/atbconfig.xml @@ -29,31 +29,16 @@ of 4. An entry of the form option has a weight of 1. format. --> - M - LL - H + L - LL - H - ML + M - LLL - MM - A - HL - MLL - HM + H - MML - HLL - HH - AL - MMM - HML - AM + A @@ -64,83 +49,81 @@ of 4. An entry of the form option has a weight of 1. - LLLL + LLLL LLLM - LLMM + LLMM + LLMH - LLMM - LMMM + LMMH MMMM - MMMH + MMMH + MMHH - MMHH - MHHH + MHHH HHHH + MHHA HHHA + MHAA HHAA - HAAA + HAAA AAAA - LLLLL - LLLLM - LLLMM + LLLLL + LLLLM + LLMMM + LLMMH - LLMMM LMMMM MMMMM - MMMMH + MMMMH MMMHH MMHHH MHHHH HHHHH - HHHHA + MHHHA MHHAA + HHHHA HHHAA - HHAAA - AAAAA + HHAAA LLLLLL - LLLLLM LLLLMM - LLLMMM + LLLMMM + LLLMHH - LLLMMM - LLMMMM - LMMMMM - MMMMMM - MMMMMH - MMMMHH + LLMMHH + MMMMMM + MMMMHH + MMMHHH - MMMHHH - MMHHHH - MHHHHH - HHHHHH - HHHHHA - HHHHAA + MMHHHH + HHHHHH + MMHHAA + HHHHAA + MMHAAA HHHAAA - HHAAAA - HAAAAA + HHAAAA AAAAAA diff --git a/MekHQ/resources/mekhq/resources/AtBContract.properties b/MekHQ/resources/mekhq/resources/AtBContract.properties new file mode 100644 index 0000000000..cf3567b0e9 --- /dev/null +++ b/MekHQ/resources/mekhq/resources/AtBContract.properties @@ -0,0 +1,15 @@ +# initiateBatchall +incomingTransmission.title=++INCOMING TRANSMISSION++ + +starColonel.text=Star Colonel +batchallOpener.text=
%s
"I am %s %s commanding the %s forces on %s.
+batchallCloser.text=

Do you accept the Batchall?
+responseAccept.text=Accept Batchall +responseAccept.tooltip=The scenarios for this contract will be balanced to roughly match your forces. +responseRefuse.text=Refuse Batchall +responseRefuse.tooltip=You will face the full strength of the Clans during this contract, and they will regard you less favorably in future dealings. +responseBringItOn.text=Bring It On +responseBringItOn.tooltip=You will face the full strength of the Clans during this contract. + +refusalConfirmation.text=Are you sure? %s will not forget this betrayal. +refusalReport.text=
YOU DARE TO REFUSE MY BATCHALL!?!
\ No newline at end of file diff --git a/MekHQ/resources/mekhq/resources/AtBDynamicScenarioFactory.properties b/MekHQ/resources/mekhq/resources/AtBDynamicScenarioFactory.properties new file mode 100644 index 0000000000..062eb7148e --- /dev/null +++ b/MekHQ/resources/mekhq/resources/AtBDynamicScenarioFactory.properties @@ -0,0 +1,7 @@ +# reportResultsOfBidding +bidAwayForcesVerbose.text=%s (%s/%s) has bid away the following forces:

%s
+bidAwayForces.text=%s (%s/%s) has bid away %s units. +addedBattleArmorNewReport.text=%s (%s/%s) has supplemented their force with %s additional unit/s of Battle Armor. +addedBattleArmorContinueReport.text=
They also supplemented their force with %s additional unit/s of Battle Armor. +batchallConcludedVersion1.text=

"Bargained Well and Done." +batchallConcludedVersion2.text=

"Well-Bargained and Done." diff --git a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties index 2cd8013647..bcd8e0849d 100644 --- a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties +++ b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties @@ -898,6 +898,13 @@ chkAdjustPaymentForStrategy.text=Adjust contract payment for deployment limits chkAdjustPaymentForStrategy.toolTipText=If the number of lances required to be deployed exceeds the current commander's strategy skill,
reduce the number when calculating new contracts and reduce the payment proportionally. lblAdditionalStrategyDeployment.text=Per rank of strategy: spnAdditionalStrategyDeployment.toolTipText=The number of additional lances that can be deployed for each increase in strategy skill. +chkUseGenericBattleValue.text=Use Force Generation 3 +chkUseGenericBattleValue.toolTipText=Bot forces are balanced used Generic Battle Value, an estimation of the average battle value for a unit of that type and weight.\ + \ This ignores pilot skill, meaning contracts against Green and Elite OpFors should feel fundamentally different.\ + \ Similarly, OpFors with higher or lower than average equipment (such as Clans or Pirates) will present higher or lower difficulty scenarios. +chkUseVerboseBidding.text=Use Verbose Bidding +chkUseVerboseBidding.toolTipText=When Generic BV is in use, Clan OpFors will engage in bidding prior to the scenario.\ + \ If this option is enabled, a list of all units bid away will be provided. chkUseVehicles.text=Use vehicles chkUseVehicles.toolTipText=Enemy forces can include vehicles. chkClanVehicles.text=Clan OpFors use vehicles diff --git a/MekHQ/resources/mekhq/resources/FameAndInfamy.properties b/MekHQ/resources/mekhq/resources/FameAndInfamy.properties new file mode 100644 index 0000000000..2257a94cd2 --- /dev/null +++ b/MekHQ/resources/mekhq/resources/FameAndInfamy.properties @@ -0,0 +1,463 @@ +# updateFameForFaction +fameChangeReportInfamy.text=You are now at Infamy %s with %s. + +# Batchall Statements +greetingFormatBatchall.text=greeting%s%sLevel%sType%s.text + +greetingCBSLevel0Type0.text=Let us craft a battle worthy of our ancestors. +greetingCBSLevel0Type1.text=Stand proud, and let us clash with all our might. +greetingCBSLevel0Type2.text=Together, let us write a new chapter in history. +greetingCBSLevel1Type0.text=Stand ready, and let us test each other in battle. +greetingCBSLevel1Type1.text=Show us your strength, and may the best prevail. +greetingCBSLevel1Type2.text=Let us meet on the field with honor. +greetingCBSLevel2Type0.text=Defend if you must; it changes nothing. +greetingCBSLevel2Type1.text=We claim what is ours, but your presence hardly matters to us. +greetingCBSLevel2Type2.text=You may stand against us, but it will make no difference in the end. +greetingCBSLevel3Type0.text=Your defense will be as mist before us: fleeting. +greetingCBSLevel3Type1.text=Blood calls for blood, but your efforts barely deserve acknowledgment. +greetingCBSLevel3Type2.text=You oppose Clan Blood Spirit? We shall erase this insult swiftly. +greetingCBSLevel4Type0.text=We have no time for the weak. What pitiful forces dare stand against us? +greetingCBSLevel4Type1.text=Blood calls for blood, yet yours is unworthy of the challenge. Face us if you dare. +greetingCBSLevel4Type2.text=We demand what is ours. Your resistance is nothing but a stain to be washed away. + + +greetingCBLevel0Type0.text=We admire your tenacity. Reveal your defenders, so we may face each other as equals. +greetingCBLevel0Type1.text=yet you stand strong. May this Trial be remembered for your bravery and strength. +greetingCBLevel0Type2.text=We honor your resolve. Let us create a battle that will echo through the ages. +greetingCBLevel1Type0.text=We recognize your strength. Identify your forces, and let us engage in honorable combat. +greetingCBLevel1Type1.text=You have chosen to stand before us. May our Trial be one worthy of remembrance. +greetingCBLevel1Type2.text=We respect your courage. Stand firm and let us see who endures. +greetingCBLevel2Type0.text=We advance. Stand in our way, or do not; it changes nothing. +greetingCBLevel2Type1.text=Your resistance is noted but irrelevant. We take what we desire. +greetingCBLevel2Type2.text=We claim this prize, but your defense holds no significance. +greetingCBLevel3Type0.text=We see little challenge here. Do you truly think you can oppose us? +greetingCBLevel3Type1.text=Your efforts are but a whisper against the strength of stone. Prepare to be ignored. +greetingCBLevel3Type2.text=You are nothing but an inconvenience. We shall cast you aside with ease. +greetingCBLevel4Type0.text=We stand unbroken, unlike your futile defense. +greetingCBLevel4Type1.text=We have endured ages, and you think to stop us? Prepare to be shattered. +greetingCBLevel4Type2.text=Your resistance is as soft as sand. We will grind you to dust. + + +greetingCCCLevel0Type0.text=We marvel at your courage. Declare your defenses, and let us meet in an honorable storm. +greetingCCCLevel0Type1.text=You have shown spirit. Let us see who can stand against the might of the Cobra. +greetingCCCLevel0Type2.text=Your bravery is commendable. Let us create a Trial that will be remembered in history. +greetingCCCLevel1Type0.text=We respect your resolve. Show us your defenses, and face the storm with pride. +greetingCCCLevel1Type1.text=You face us with courage. May this Trial be one of honor. +greetingCCCLevel1Type2.text=You have shown strength. Let us clash and see who stands victorious. +greetingCCCLevel2Type0.text=Stand if you wish; it makes no difference. +greetingCCCLevel2Type1.text=This target is ours. +greetingCCCLevel2Type2.text=You may show yourself, but it will not change the outcome. +greetingCCCLevel3Type0.text=You think to stand before us? We will barely feel your resistance. +greetingCCCLevel3Type1.text=Your presence is an annoyance. We will brush you aside with ease. +greetingCCCLevel3Type2.text=You are unworthy of our attention, but we shall remove you nonetheless. +greetingCCCLevel4Type0.text=What insignificant force dares oppose us? +greetingCCCLevel4Type1.text=You are but insects before the storm. Prepare to be swept away. +greetingCCCLevel4Type2.text=Your defense is a mere breeze against our power. + + +greetingCCOLevel0Type0.text=We admire your strength. Reveal yourselves, and let us engage in a battle worthy of legend. +greetingCCOLevel0Type1.text=You face us with the courage of a warrior. Let this Trial honor both our strengths. +greetingCCOLevel0Type2.text=We honor your tenacity. May this be a hunt that will be sung of for years to come. +greetingCCOLevel1Type0.text=We honor worthy prey. Reveal yourselves, and let us see if you can match our cunning. +greetingCCOLevel1Type1.text=You have shown courage by standing before us. Let us see if you are a worthy challenge. +greetingCCOLevel1Type2.text=We respect your spirit. Meet us in battle, and may the best hunter prevail. +greetingCCOLevel2Type0.text=Whether you stand or flee, it changes nothing. +greetingCCOLevel2Type1.text=You are but another obstacle in our hunt. We shall take what we desire. +greetingCCOLevel2Type2.text=Your defense is noted but irrelevant. We move forward regardless. +greetingCCOLevel3Type0.text=You cannot hope to outwit us. Your resistance is futile. +greetingCCOLevel3Type1.text=Your attempts at defense are laughable. We will tear through them with ease. +greetingCCOLevel3Type2.text=You dare to stand before us? We will enjoy this brief challenge. +greetingCCOLevel4Type0.text=We hunger, and you will be our prey. Resist if you dare. +greetingCCOLevel4Type1.text=You are nothing but a shadow in our path. Prepare to be devoured. +greetingCCOLevel4Type2.text=We hunt without mercy. Your defenses will crumble before us. + + +greetingCDSVersion1Level0Type0.text=You face us with the spirit of a predator. Let this battle be one that echoes through the tides. +greetingCDSVersion1Level0Type1.text=Your strength shines like the hard ridges of our own form. Together, let us carve a path worthy of remembrance. +greetingCDSVersion1Level0Type2.text=You swim fearlessly in dangerous waters. Let us fight as equals, and create a Trial worthy of the deep. +greetingCDSVersion1Level1Type0.text=You stand with courage. Show us your strength, and we will test it against our own. +greetingCDSVersion1Level1Type1.text=You move with the flow, yet stand firm. Let us see if you can withstand the weight of our jaws. +greetingCDSVersion1Level1Type2.text=We acknowledge a worthy challenge. May this Trial prove who truly commands these waters. +greetingCDSVersion1Level2Type0.text=Resist if you wish, but it changes nothing; we will have what we desire. +greetingCDSVersion1Level2Type1.text=You are but another creature in our waters. Stand, flee, or fall - it matters little to us. +greetingCDSVersion1Level2Type2.text=You are already in our grasp. Move, struggle, or yield; we will take what belongs to us. +greetingCDSVersion1Level3Type0.text=You thrash in the waters, unaware that we have already begun our feast. Your end is inevitable. +greetingCDSVersion1Level3Type1.text=We strike fast, and you will be left in pieces. +greetingCDSVersion1Level3Type2.text=You think you stand a chance? We do not wait; we seize what we want and leave nothing behind. +greetingCDSVersion1Level4Type0.text=You are but prey. We will wait until you bleed out before claiming what is ours. +greetingCDSVersion1Level4Type1.text=Your resistance is feeble. Like flesh against diamond, you will break, and we shall take what is ours. +greetingCDSVersion1Level4Type2.text=You cannot escape the inevitable. The waters belong to us now; resist, and we will cut you down. + +greetingCDSVersion2Level0Type0.text=You rise with the power of a thousand waves. We bow, then challenge you - let this be a battle worthy of our ancestors. +greetingCDSVersion2Level0Type1.text=Your strength flows like the ocean's heartbeat. Together, we will create a story that dances across the waves. +greetingCDSVersion2Level0Type2.text=The Sea Fox bows deeply, in awe of your spirit. Let this Trial be a dance that lives on in the waters forever. +greetingCDSVersion2Level1Type0.text=You face us with the strength of warriors. We bow, then raise our voices - let our Trial be one to remember. +greetingCDSVersion2Level1Type1.text=You stand firm as the tide rises. We honor you with this challenge; may you match the power of the waves. +greetingCDSVersion2Level1Type2.text=The Sea Fox respects your courage. Let our cries and steps echo together as we test our might. +greetingCDSVersion2Level2Type0.text=The Sea Fox moves as the ocean commands. Your resistance is but a ripple; we will flow over you. +greetingCDSVersion2Level2Type1.text=We chant and call, but it is not for you - it is for the journey. Stand or fall, the waves endure. +greetingCDSVersion2Level2Type2.text=The Sea Fox does not stop. You are but a shadow on the water, soon to be forgotten. +greetingCDSVersion2Level3Type0.text=You shiver like a leaf in the wind. The Sea Fox steps forward, and soon, your fear will be realized. +greetingCDSVersion2Level3Type1.text=Your efforts are weak; you tremble before us. We shall break you, as the ocean smashes rock. +greetingCDSVersion2Level3Type2.text=We dance on the water's edge, and you falter. Prepare to meet the force of the storm. +greetingCDSVersion2Level4Type0.text=The Sea Fox bows but knows that this is no greeting - it is a warning. Your fate is sealed, and the sea will consume you. +greetingCDSVersion2Level4Type1.text=You are but driftwood, tossed by the waves. We stomp and call out, preparing to tear you apart. +greetingCDSVersion2Level4Type2.text=The tide rises, the Sea Fox snarls. There is no escape - you will be swallowed whole. + + +greetingCFMLevel0Type0.text=We honor your valor. Show us your defenses, and let us see who burns the brightest. +greetingCFMLevel0Type1.text=You stand before us with courage. Let us create a blaze that will light the path for others. +greetingCFMLevel0Type2.text=Your spirit is strong. May this Trial be as fierce as the flames that guide us. +greetingCFMLevel1Type0.text=We acknowledge your courage. Stand firm, and be tested in the fires of battle. +greetingCFMLevel1Type1.text=You face us with bravery. Let us burn brightly together in this Trial. +greetingCFMLevel1Type2.text=We honor your strength. May our flames clash and reveal who is strongest. +greetingCFMLevel2Type0.text=We burn bright. Defend if you dare, or step aside. +greetingCFMLevel2Type1.text=Your resistance will be consumed all the same. Stand, or do not. +greetingCFMLevel2Type2.text=You matter little to us. We take what is ours, regardless. +greetingCFMLevel3Type0.text=Your efforts barely deserve our notice. +greetingCFMLevel3Type1.text=You are but kindling to our flame. Your resistance will be short-lived. +greetingCFMLevel3Type2.text=We burn brighter than you will ever understand. Stand aside or be consumed. +greetingCFMLevel4Type0.text=You are unworthy of our flame. Prepare to be consumed. +greetingCFMLevel4Type1.text=Your defenses will fuel our fire, leaving nothing but ash. +greetingCFMLevel4Type2.text=We burn through all. What pathetic resistance will you offer? + + +greetingCGBVersion1Level0Type0.text=We are impressed by your resolve. Identify your forces, so that we may share this Trial. +greetingCGBVersion1Level0Type1.text=You have shown the heart of a warrior. Let us test one another in this honored battle. +greetingCGBVersion1Level0Type2.text=Your courage is an inspiration. Together, let us make this a battle to be remembered. +greetingCGBVersion1Level1Type0.text=We respect your strength. Declare your forces, and we will meet you in battle. +greetingCGBVersion1Level1Type1.text=You have shown bravery. Let this be a Trial worthy of our ancestors. +greetingCGBVersion1Level1Type2.text=We acknowledge your courage. Stand before us, and let us see who is truly strong. +greetingCGBVersion1Level2Type0.text=Identify your forces or be forgotten. +greetingCGBVersion1Level2Type1.text=We claim what we desire. Your stand is of little concern. +greetingCGBVersion1Level2Type2.text=We care not for your defense. We will proceed. +greetingCGBVersion1Level3Type0.text=We see through your weakness. Declare your forces, or be removed. +greetingCGBVersion1Level3Type1.text=You amount to little more than a fleeting challenge. Prepare to fall. +greetingCGBVersion1Level3Type2.text=Your efforts are inconsequential. We shall take what is ours. +greetingCGBVersion1Level4Type0.text=We dismiss your efforts. Reveal your forces, or be swept aside. +greetingCGBVersion1Level4Type1.text=You are nothing but prey, and we are the hunters. Identify yourselves. +greetingCGBVersion1Level4Type2.text=Your resistance is beneath notice. Prepare to be obliterated. + + +greetingCGSLevel0Type0.text=You seek it as if it were your own, and we admire that strength. Together, let us see who is worthy of this prize. +greetingCGSLevel0Type1.text=You stand as a true warrior before what we seek. Let this battle be worthy of the artifact's legacy. +greetingCGSLevel0Type2.text=Your defense is noble, but the Goliath Scorpion's desire is relentless. Let us determine who the rightful keeper is. +greetingCGSLevel1Type0.text=You have admirable strength. Prove your worth, and let this Trial determine who holds the greater claim. +greetingCGSLevel1Type1.text=You stand between us and our heritage. We respect your courage but know this: we shall not be denied. +greetingCGSLevel1Type2.text=This artifact is ours to reclaim. Face us with honor, and let this battle determine its true guardian. +greetingCGSLevel2Type0.text=The artifact you seek will be ours. Your presence is merely a delay, not a deterrent. +greetingCGSLevel2Type1.text=We desire something of value, but your life does not concern us. Stand aside, or be removed. +greetingCGSLevel2Type2.text=The prize we seek lies beyond you. Move, or be swept away. +greetingCGSLevel3Type0.text=You think you can protect what the Goliath Scorpion desires? We will carve you from its side. +greetingCGSLevel3Type1.text=Your grip is weak, and your resolve weaker. Prepare to be cast aside as we claim what belongs to us. +greetingCGSLevel3Type2.text=You attempt to shield the past from us, but you will soon learn that nothing escapes our reach. +greetingCGSLevel4Type0.text=You desire what belongs to us. Step aside, or feel the venom of the Goliath Scorpion as we take back our legacy. +greetingCGSLevel4Type1.text=Your hands are unworthy to hold such relics. We will strike you down and reclaim what is ours by right. +greetingCGSLevel4Type2.text=You stand before what we seek. Know that we will stop at nothing to possess it. + + +greetingCHHLevel0Type0.text=We see in you the fire that drives warriors forward. May this Trial be one that the stars themselves shall remember. +greetingCHHLevel0Type1.text=Your strength matches our own. Let us clash and create a story that will be remembered by the wind and the earth. +greetingCHHLevel0Type2.text=You stand as one who understands the spirit of the wild. Together, let us create a battle that echoes across the ages. +greetingCHHLevel1Type0.text=We honor your courage. The Hell's Horses will meet you in battle, and let the strongest prevail. +greetingCHHLevel1Type1.text=You stand firm, as few have before. Let our clash be a testament to the strength of man and beast. +greetingCHHLevel1Type2.text=You face us with the heart of a warrior. Let this battle be worthy of the blood and fury that drives us. +greetingCHHLevel2Type0.text=Our path is clear, and you are but an echo of defiance. We shall trample it into the dust. +greetingCHHLevel2Type1.text=You are but a shadow in the wind. We ride to victory, and you will be left behind. +greetingCHHLevel2Type2.text=We charge forward, and your resistance means nothing. Stand or fall; it matters not to the Hell's Horses." +greetingCHHLevel3Type0.text=Your forces are weak, like grass before the hooves of the horse. We will ride over you without pause. +greetingCHHLevel3Type1.text=You attempt to tame what cannot be tamed. We will show you the wrath of the untamed herd. +greetingCHHLevel3Type2.text=You are but a stone in our path. The Hell's Horses will shatter you as we have countless others. +greetingCHHLevel4Type0.text=You think yourself strong, but you will be broken like all who stand before our stampede. +greetingCHHLevel4Type1.text=You cannot match our fury. We will trample you underfoot, as the horse crushes bones in its charge. +greetingCHHLevel4Type2.text=You are merely prey. We shall ride you down and leave nothing but dust. + + +greetingCIHLevel0Type0.text=We are impressed by your courage. Reveal your forces, and let us test our mettle. +greetingCIHLevel0Type1.text=You face us with bravery. Let this Trial be as fierce and unyielding as ice. +greetingCIHLevel0Type2.text=Your spirit is admirable. Together, let us create a battle worth remembering. +greetingCIHLevel1Type0.text=We acknowledge your readiness. Reveal your forces, and face the fury of our onslaught. +greetingCIHLevel1Type1.text=You have shown resolve. Let us see if you can withstand our speed and precision. +greetingCIHLevel1Type2.text=We respect your stand. May this Trial be as fierce as the Ice Hellion's bite. +greetingCIHLevel2Type0.text=We descend. Your resistance is of no consequence. +greetingCIHLevel2Type1.text=We strike regardless of your presence. Stand or move aside. +greetingCIHLevel2Type2.text=You mean little to us. We will take this prize without delay. +greetingCIHLevel3Type0.text=We regard you with indifference. Stand if you wish, but you will fall. +greetingCIHLevel3Type1.text=You are but a whisper in the wind. We will sweep you away. +greetingCIHLevel3Type2.text=Your resistance is but ice beneath our claws. It will shatter. +greetingCIHLevel4Type0.text=We care not for your resistance. Prepare to be frozen in fear. +greetingCIHLevel4Type1.text=Your defenses will shatter like ice before us. Stand if you wish. +greetingCIHLevel4Type2.text=Our fury is upon you. You will be swept away. + + +greetingCJFLevel0Type0.text=We admire worthy adversaries. Show us your defenses, and face our talons with pride. +greetingCJFLevel0Type1.text=You face us with the heart of a true warrior. Let us make this a battle of legends. +greetingCJFLevel0Type2.text=Your strength inspires us. May this Trial be one that honors both our names. +greetingCJFLevel1Type0.text=We honor worthy opponents. Show us your defenses, and let our talons test your mettle. +greetingCJFLevel1Type1.text=You have chosen to face the Jade Falcon. May your stand be worthy of song. +greetingCJFLevel1Type2.text=We respect your courage. Let us see who earns the sky today. +greetingCJFLevel2Type0.text=We claim this prize. Stand if you choose; it matters not. +greetingCJFLevel2Type1.text=You may defend yourself, but it changes nothing. We will proceed. +greetingCJFLevel2Type2.text=The talons of the Jade Falcon reach forth. Your resistance is trivial. +greetingCJFLevel3Type0.text=We disregard you. Who believes they can stand against us? +greetingCJFLevel3Type1.text=Your defenses are nothing more than a fleeting distraction. +greetingCJFLevel3Type2.text=You are but prey beneath our talons. Prepare to fall. +greetingCJFLevel4Type0.text=We scoff at your pitiful defenses. Prepare to be torn apart. +greetingCJFLevel4Type1.text=You are but prey, awaiting our talons. Reveal yourselves. +greetingCJFLevel4Type2.text=We do not ask; we take. Stand, or be crushed. + + +greetingCMGLevel0Type0.text=We respect your agility. Identify yourselves, and let us test our swiftness together. +greetingCMGLevel0Type1.text=You have shown incredible speed. Let us see who truly commands the hunt. +greetingCMGLevel0Type2.text=We admire your tenacity. May this Trial be one for the annals of history. +greetingCMGLevel1Type0.text=We respect your defenses. Identify yourselves, and let us see who is truly swift. +greetingCMGLevel1Type1.text=You have proven to be quick-witted. Let us match our speed against yours. +greetingCMGLevel1Type2.text=We honor your agility. Stand before us and may the swiftest emerge victorious. +greetingCMGLevel2Type0.text=We move swiftly. Show yourselves, or be ignored. +greetingCMGLevel2Type1.text=Your defenses mean nothing to us. We will take what we seek. +greetingCMGLevel2Type2.text=We move forward, whether you stand or not. It matters little. +greetingCMGLevel3Type0.text=We find you insignificant. Show yourselves, or be hunted down. +greetingCMGLevel3Type1.text=Your presence is barely worth acknowledging. Stand if you must. +greetingCMGLevel3Type2.text=You are nothing but prey in our path. We will strike you down. +greetingCMGLevel4Type0.text=We strike without mercy. You will not stand in our way. +greetingCMGLevel4Type1.text=Your defenses mean nothing. Prepare to be hunted. +greetingCMGLevel4Type2.text=You are but prey in our path. Identify yourselves or be swept aside. + + +greetingCNCLevel0Type0.text=You burn with the light of a thousand suns. Together, let us create a battle that shines across the ages. +greetingCNCLevel0Type1.text=Your spirit dances like fire in the darkness. Let us meet and write our story in the stars. +greetingCNCLevel0Type2.text=We honor your brilliance. May our Trial be a blaze that the universe itself will remember. +greetingCNCLevel1Type0.text=You face us with the spirit of one who understands the flame. Let our Trial be one that lights the heavens. +greetingCNCLevel1Type1.text=You have chosen to stand against the Nova Cat. We honor your courage, and may the fire of battle burn brightly. +greetingCNCLevel1Type2.text=We see your strength, and it mirrors our own. Let this clash be worthy of the stars. +greetingCNCLevel2Type0.text=Our path is clear, and you are but an ember in the night. We shall step over you. +greetingCNCLevel2Type1.text=The Nova Cat walks through the darkness, unbothered by your flickering resistance. +greetingCNCLevel2Type2.text=You are but a momentary shadow. We shall pass by, leaving only scorched earth in our wake. +greetingCNCLevel3Type0.text=You hide from our light, but your darkness is shallow. We will burn through and reveal your weakness. +greetingCNCLevel3Type1.text=The Nova Cat wastes no time with prey that cannot see. You are already lost in the flames. +greetingCNCLevel3Type2.text=You stand before us, blind to your fate. We shall sweep you aside like ash in the wind. +greetingCNCLevel4Type0.text=The Nova Cat's eyes burn bright. You are but a shadow, soon to be scorched from existence. +greetingCNCLevel4Type1.text=We see through your defenses, and they mean nothing. The flames of our vision will consume you. +greetingCNCLevel4Type2.text=In the darkness, you cower, but the Nova Cat's gaze pierces all. Prepare to be undone. + + +greetingCSJLevel0Type0.text=We commend your courage. Identify your forces, and let us battle with all our might. +greetingCSJLevel0Type1.text=You have proven yourself worthy of our claws. Let this Trial be one of legends. +greetingCSJLevel0Type2.text=We honor your spirit. May this be a fierce and worthy battle. +greetingCSJLevel1Type0.text=We respect your courage. Identify your forces, and face our fury with pride. +greetingCSJLevel1Type1.text=You stand against us with bravery. Let us see if you can match our ferocity. +greetingCSJLevel1Type2.text=We honor your strength. May this Trial be as fierce as our claws. +greetingCSJLevel2Type0.text=We prowl onward. Your resistance is irrelevant. +greetingCSJLevel2Type1.text=We strike whether you stand or flee. It matters little to us. +greetingCSJLevel2Type2.text=You are but prey, and we will proceed regardless. +greetingCSJLevel3Type0.text=We disregard you. Identify your forces, or be swept away. +greetingCSJLevel3Type1.text=Your defenses are a mere annoyance. We will tear through you. +greetingCSJLevel3Type2.text=You are not worthy of our attention. Prepare to be destroyed. +greetingCSJLevel4Type0.text=Our fury is unmatched. You will not survive. +greetingCSJLevel4Type1.text=Your defenses will crumble before us. Prepare to be annihilated. +greetingCSJLevel4Type2.text=We do not tolerate weakness. Stand and be destroyed. + + +greetingCSRLevel0Type0.text=You face us with the spirit of a true warrior. Let this Trial be remembered in the songs of our people. +greetingCSRLevel0Type1.text=You stand proud, and the Snow Raven bows in admiration. Together, we shall create a story for the ages. +greetingCSRLevel0Type2.text=Your strength echoes across the sky. Let this be a Trial worthy of the ancestors who watch over us. +greetingCSRLevel1Type0.text=You stand with purpose, and the Snow Raven acknowledges this. Let us see if you are worthy of the feast. +greetingCSRLevel1Type1.text=You have proven to be more than mere scraps. We shall meet you as equals in this Trial. +greetingCSRLevel1Type2.text=The Snow Raven respects your strength. May our clash be one of honor and purpose. +greetingCSRLevel2Type0.text=The Snow Raven does not concern itself with your resistance. We will take what you leave behind. +greetingCSRLevel2Type1.text=You are but a moment in the wind. We shall continue our path, unbothered by your presence. +greetingCSRLevel2Type2.text=Our gaze has turned to you, but it matters little. The outcome is already decided. +greetingCSRLevel3Type0.text=You offer little but bones. We will pick them clean as we always do. +greetingCSRLevel3Type1.text=You stand before the Snow Raven, yet you have nothing to offer but your remains. +greetingCSRLevel3Type2.text=You are but a small morsel, and we will make use of even that. Prepare to be taken. +greetingCSRLevel4Type0.text=The Snow Raven circles, seeing your weakness. You are but scraps left behind, soon to be taken. +greetingCSRLevel4Type1.text=You waste what you have, but we waste nothing. We shall claim what you leave behind. +greetingCSRLevel4Type2.text=Your presence is like carrion; the Snow Raven will strip you to the bone. + + +greetingCSALevel0Type0.text=We respect your tenacity. Reveal your defenses, and let us face each other with honor. +greetingCSALevel0Type1.text=You have proven yourself worthy of our strike. Let us make this a Trial to remember. +greetingCSALevel0Type2.text=We admire your strength. Let this encounter be one of the greatest Trials. +greetingCSALevel1Type0.text=We respect your strength. Present your defenses, and prepare for a true challenge. +greetingCSALevel1Type1.text=You face us with bravery. May this Trial be as fierce as our venom. +greetingCSALevel1Type2.text=We acknowledge your strength. Let us see who will earn the right to this prize. +greetingCSALevel2Type0.text=Defend if you wish, but it changes nothing. +greetingCSALevel2Type1.text=You are insignificant to our advance. We strike regardless. +greetingCSALevel2Type2.text=Your resistance is noted but unimportant. We will claim what is ours. +greetingCSALevel3Type0.text=We find you unworthy. Present yourselves, or be crushed. +greetingCSALevel3Type1.text=You are but prey before us. Your resistance will be for nothing. +greetingCSALevel3Type2.text=You stand against us? Prepare to be destroyed. +greetingCSALevel4Type0.text=We strike with venom. You will not survive our bite. +greetingCSALevel4Type1.text=Your defenses are laughable. Prepare to be crushed. +greetingCSALevel4Type2.text=You are prey caught in our coils. Identify yourselves. + + +greetingCSVLevel0Type0.text=We admire your strength. Show yourselves, and let us clash in battle. +greetingCSVLevel0Type1.text=You have faced us with courage. May this Trial prove the strength of both our wills. +greetingCSVLevel0Type2.text=Your resolve is commendable. Let us create a battle that will echo through time. +greetingCSVLevel1Type0.text=We acknowledge your resolve. Stand ready, and let us test our fangs. +greetingCSVLevel1Type1.text=You have shown strength. May this Trial prove who is truly worthy. +greetingCSVLevel1Type2.text=We respect your stand. Let us see who survives this battle of steel. +greetingCSVLevel2Type0.text=We slither forward. Your stand is meaningless. +greetingCSVLevel2Type1.text=You will be crushed whether you resist or not. We will take this prize. +greetingCSVLevel2Type2.text=You are but prey. We advance regardless of your defense. +greetingCSVLevel3Type0.text=We pay you no mind. Reveal your forces, or fall swiftly. +greetingCSVLevel3Type1.text=You will be crushed under our coils. You are but a hindrance. +greetingCSVLevel3Type2.text=You are nothing but prey to be constricted and eliminated. +greetingCSVLevel4Type0.text=We will strike you down. You have no chance to survive. +greetingCSVLevel4Type1.text=Your efforts are futile. We will constrict you until nothing remains. +greetingCSVLevel4Type2.text=You are unworthy of our notice. Prepare to be eradicated. + + +greetingCSLLevel0Type0.text=We honor your courage. Present your defenses, and let us fight with pride. +greetingCSLLevel0Type1.text=You have shown the spirit of a lion. Let us meet in battle, as true warriors. +greetingCSLLevel0Type2.text=We respect your strength. May this be a Trial worthy of our ancestors. +greetingCSLLevel1Type0.text=We respect your courage. Reveal your defenses, and face us with pride. +greetingCSLLevel1Type1.text=You stand before us with bravery. Let us test each other's strength. +greetingCSLLevel1Type2.text=We honor your spirit. Let this Trial determine who is truly worthy. +greetingCSLLevel2Type0.text=Whether you stand or yield means nothing. +greetingCSLLevel2Type1.text=You are but dust in our path. We claim this goal regardless. +greetingCSLLevel2Type2.text=Your defense is but a minor inconvenience. We move forward. +greetingCSLLevel3Type0.text=We find your resistance laughable. Prepare for defeat. +greetingCSLLevel3Type1.text=You will be a mere notch on our claws. Stand if you dare. +greetingCSLLevel3Type2.text=Your defenses are pebbles before us. We will crush you. +greetingCSLLevel4Type0.text=We pounce. You will not survive our fury. +greetingCSLLevel4Type1.text=Your defenses are mere pebbles. We will crush you. +greetingCSLLevel4Type2.text=We show no mercy. Stand and be torn apart. + + +greetingCWILevel0Type0.text=We respect your valor. Declare your forces, and let us engage in worthy combat. +greetingCWILevel0Type1.text=You have shown courage. Let us create a Trial that honors both our legacies. +greetingCWILevel0Type2.text=We admire your strength. Together, let us create a battle that will be sung of. +greetingCWILevel1Type0.text=We respect your valor. Declare your forces, and meet us with dignity. +greetingCWILevel1Type1.text=You have shown courage. May this battle be worthy of our ancestors. +greetingCWILevel1Type2.text=We acknowledge your strength. Let us engage in combat befitting warriors. +greetingCWILevel2Type0.text=Whether you resist or not, you will fall. +greetingCWILevel2Type1.text=Your presence is irrelevant. We take what belongs to us. +greetingCWILevel2Type2.text=You are but another thread in our web. We will take what we want. +greetingCWILevel3Type0.text=We care little for your stand. Declare your forces, or be destroyed. +greetingCWILevel3Type1.text=You will be trapped in our web. Your resistance is futile. +greetingCWILevel3Type2.text=You are but a distraction. We will eradicate you. +greetingCWILevel4Type0.text=You will not survive this encounter. +greetingCWILevel4Type1.text=Your defense is meaningless. Prepare to be swept aside. +greetingCWILevel4Type2.text=You are but prey in our web. Prepare for destruction. + + +greetingCWLevel0Type0.text=We admire your strength. Identify yourselves, and let us meet as true warriors. +greetingCWLevel0Type1.text=You have shown the heart of a warrior. Let this be a Trial that echoes through time. +greetingCWLevel0Type2.text=We respect your spirit. May this be a battle worthy of our ancestors. +greetingCWLevel1Type0.text=We respect the strength of those who stand. Show us your forces, and face us with pride. +greetingCWLevel1Type1.text=You have chosen to face the Wolves. Let us see who earns the right to this land. +greetingCWLevel1Type2.text=We honor your resolve. Let us meet in a battle worthy of legends. +greetingCWLevel2Type0.text=Your resistance is trivial at best. +greetingCWLevel2Type1.text=Stand if you wish; we take this land regardless. +greetingCWLevel2Type2.text=Your defense means nothing. We claim what is ours. +greetingCWLevel3Type0.text=We see only tame dogs before us. You dare to challenge us? +greetingCWLevel3Type1.text=Your defenses are barely worth our time. Stand aside or be destroyed. +greetingCWLevel3Type2.text=You amount to nothing before us. Prepare to be devoured. +greetingCWLevel4Type0.text=We have no patience for the weak. Reveal yourselves. +greetingCWLevel4Type1.text=You are mere sheep before us. Prepare to be devoured. +greetingCWLevel4Type2.text=Your resistance is pitiful. We will tear through you. + + +greetingCWIELevel0Type0.text=We are impressed by your resolve. Reveal your defenses, and let us reclaim our honor together. +greetingCWIELevel0Type1.text=You stand before us with courage. Let this Trial be one to remember. +greetingCWIELevel0Type2.text=We admire your bravery. Let us engage in battle as warriors of old. +greetingCWIELevel1Type0.text=We honor worthy defenders. Reveal yourselves, and let us reclaim what is rightfully ours. +greetingCWIELevel1Type1.text=You stand before us with courage. May this Trial be one to remember. +greetingCWIELevel1Type2.text=We respect your strength. Let us engage and determine who truly deserves this place. +greetingCWIELevel2Type0.text=Your efforts are meaningless. +greetingCWIELevel2Type1.text=We reclaim what belongs to us, regardless of your resistance. +greetingCWIELevel2Type2.text=Your presence is of little concern. We will take what is ours. +greetingCWIELevel3Type0.text=We find you irrelevant. Reveal yourselves, or be cast out. +greetingCWIELevel3Type1.text=You have no place before us. We will reclaim what is ours. +greetingCWIELevel3Type2.text=Your defenses are insignificant. We will take back our ground. +greetingCWIELevel4Type0.text=We will reclaim what is ours. +greetingCWIELevel4Type1.text=Your defense means nothing. Stand or be swept away. +greetingCWIELevel4Type2.text=You cannot stop us. Prepare to be eradicated. + + +greetingCEIVersion1Level0Type0.text=You protect the past with honor, and we admire your spirit. Let us meet in a Trial worthy of the ancestors who watch over us. +greetingCEIVersion1Level0Type1.text=Your resolve shines brightly, like the stars above. Together, let us create a battle that will be remembered for generations. +greetingCEIVersion1Level0Type2.text=We honor your strength and dedication. May this Trial be one that the universe itself will speak of. +greetingCEIVersion1Level1Type0.text=You face us with the strength of a true warrior. The Scorpion Empire honors your courage; let this Trial determine who is worthy. +greetingCEIVersion1Level1Type1.text=You stand against us, and we recognize your resolve. Let this battle be one that echoes in the annals of history. +greetingCEIVersion1Level1Type2.text=We see in you a defender of what is sacred. Let this Trial decide who shall bear the legacy of the past. +greetingCEIVersion1Level2Type0.text=Your presence is but a moment in our journey. We shall take what is needed, and you will be left in the shadows. +greetingCEIVersion1Level2Type1.text=The Scorpion Empire does not stop for those who stand in our way. We will advance, and you will be swept aside. +greetingCEIVersion1Level2Type2.text=We have seen countless obstacles, and you are just another. We will take what we seek, and you will be forgotten. +greetingCEIVersion1Level3Type0.text=You hold onto relics you cannot comprehend. We, the Scorpion Empire, will take what you have squandered. +greetingCEIVersion1Level3Type1.text=You are but a fragment of the past, unworthy of what you protect. We will strip you of your illusions and take what is ours. +greetingCEIVersion1Level3Type2.text=You attempt to shield what belongs to us, but we have outlasted greater threats than you. The Empire will reclaim what you have failed to claim. +greetingCEIVersion1Level4Type0.text=You stand against an Empire that was forged in unity and strength. We will take what you desire, as we have taken all that we desire. +greetingCEIVersion1Level4Type1.text=You face the Scorpion Empire, and yet you fail to see your insignificance. We shall claim what belongs to us, as we have always done. +greetingCEIVersion1Level4Type2.text=Your defiance means nothing to us. We shall break you as we have broken all who stood before us. + + +greetingRDLevel0Type0.text=You stand with the strength of the bear, as we stand with the line of our people. May this battle be remembered by the brave who live forever. +greetingRDLevel0Type1.text=Your courage shines bright, like the Northern lights. Let us fight, knowing our fathers and mothers watch us from the halls of Valhalla. +greetingRDLevel0Type2.text=You are a warrior worthy of the Dominion's respect. Together, let us write a tale that will echo back to the beginning, where the brave may live forever. +greetingRDLevel1Type0.text=Stand firm, as we do, with the strength of our fathers and mothers who watch us from beyond. +greetingRDLevel1Type1.text=You have chosen to face us. May this battle be worthy of our ancestors, who call us from the halls of heroes. +greetingRDLevel1Type2.text=We recognize a warrior's heart in you. Reveal your strength, and let us join our ancestors in the tales of valor, where the brave live forever. +greetingRDLevel2Type0.text=We claim this prize, whether you resist or not. The bear moves, and your presence is but a passing shadow. +greetingRDLevel2Type1.text=Stand if you wish; it makes no difference. The Dominion advances, unyielding as the mountains. +greetingRDLevel2Type2.text=Your choice is yours alone. Fight, flee, or submit - it matters little to us. +greetingRDLevel3Type0.text=Your defenses are little more than leaves in the wind. +greetingRDLevel3Type1.text=We find no worth in your resistance. Like the bear swatting a fly, we will brush you aside. +greetingRDLevel3Type2.text=You think yourself strong? We shall show you the true meaning of power. +greetingRDLevel4Type0.text=Like a bear crushing twigs, we will break you. +greetingRDLevel4Type1.text=You are but prey to our strength. Defy us, and feel the weight of the bear's fury. +greetingRDLevel4Type2.text=We do not seek a challenge here. Step aside, or be trampled by the might of our claws. + + +greetingRALevel0Type0.text=You stand with the strength of those who carved their place from the stars. Together, let us create a story worth remembering. +greetingRALevel0Type1.text=Your courage shines bright, like the morning sun on an endless plain. Let us clash, and may our Trial be one of legends. +greetingRALevel0Type2.text=We honor the spirit that rises to meet us. May our battle be as vast as the open sky and as deep as the raven's cry. +greetingRALevel1Type0.text=You face us with courage, and the Raven bows its head. Let this battle be worthy of the endless horizon. +greetingRALevel1Type1.text=You have chosen to meet us, and we respect that choice. May your spirit match the open sky. +greetingRALevel1Type2.text=We honor your resolve. As pioneers of this land, let us test who has the greater claim. +greetingRALevel2Type0.text=Stand if you must; it matters not, for we always reach our destination. +greetingRALevel2Type1.text=You are but another obstacle on this long journey. Resist if you wish, but our path is already set. +greetingRALevel2Type2.text=Our course is true, and your presence is but a stone in the river. We flow forward regardless. +greetingRALevel3Type0.text=You think your defenses are strong, but they are as brittle as old wood. We will break through without pausing. +greetingRALevel3Type1.text=You stand before the Raven, yet your strength is that of a dried riverbed - cracked and weak. +greetingRALevel3Type2.text=You may try to defend this land, but like a tumbleweed, you will be carried away. +greetingRALevel4Type0.text=You are but dust on the trail. We will take what is needed and leave nothing behind. +greetingRALevel4Type1.text=You stand in our path, but like windblown sand, you will be scattered before us. +greetingRALevel4Type2.text=We waste no time with those who cannot endure. Move, or be swept aside by the coming storm. + + +greetingCPLevel0Type0.text=You burn with a light that defies the darkness. Let this battle be one that brings life to our stories once more. +greetingCPLevel0Type1.text=Your spirit shines with the brilliance of a nova. Together, we shall create a Trial that the universe itself will remember. +greetingCPLevel0Type2.text=We see in you the strength of those who fight for more than themselves. Let this battle be one that honors all who came before us. +greetingCPLevel1Type0.text=You stand with courage, as we have done many times before. Let us see if your spirit can match the strength of those who endure. +greetingCPLevel1Type1.text=You face us with eyes unclouded. We honor that bravery, and may this Trial be one that echoes through the stars. +greetingCPLevel1Type2.text=We recognize your strength, a strength that mirrors our own. Let us meet in this Trial as warriors who respect the path we walk. +greetingCPLevel2Type0.text=We have walked through fire, and your presence is but a whisper against the storm. We have no time for you. +greetingCPLevel2Type1.text=The path is clear, and you are but a fleeting obstacle. Stand or flee - it matters little to us. +greetingCPLevel2Type2.text=You are neither our enemy nor our savior. We walk a journey far greater than this moment. +greetingCPLevel3Type0.text=You see only the surface, unaware of the depths that guide us. We will strike, and you shall see the truth too late. +greetingCPLevel3Type1.text=You treat us as prey, but you forget - survivors know the path to victory. Prepare to face what you cannot understand. +greetingCPLevel3Type2.text=You mock the scars we bear, but it is through pain we have found strength. We will show you what true resilience is. +greetingCPLevel4Type0.text=The Protectorate stands as the last of our kind. You are but an echo of what was - we will silence you. +greetingCPLevel4Type1.text=You threaten what remains of us. We will show you why even the shadows fear the light of our flames. +greetingCPLevel4Type2.text=You step into a realm where survivors become hunters. We will fight with the fury of those who have lost everything. + + +greetingCLANLevel0Type0.text=We honor your resolve. May this battle be one that shines among the stars. +greetingCLANLevel0Type1.text=Your strength is worthy of respect. Together, let us create a Trial that will be remembered. +greetingCLANLevel0Type2.text=You fight with the spirit of a true warrior. Let this battle be one for the ages. +greetingCLANLevel1Type0.text=You stand with honor. May our clash be one that echoes in history. +greetingCLANLevel1Type1.text=We acknowledge your bravery. Let this Trial determine who is truly worthy. +greetingCLANLevel1Type2.text=You face us with courage. Let this battle be a true test of strength. +greetingCLANLevel2Type0.text=Your presence is insignificant. We shall take what we desire and move on. +greetingCLANLevel2Type1.text=You are but an obstacle on our path. We will advance regardless. +greetingCLANLevel2Type2.text=You may stand or fall; it matters not to us. We will take what we seek. +greetingCLANLevel3Type0.text=You are nothing before our might. Prepare to be cast aside. +greetingCLANLevel3Type1.text=Your defenses are weak and unworthy. We will break through and take what is ours. +greetingCLANLevel3Type2.text=You cannot hope to stand against us. We shall sweep you aside with little effort. +greetingCLANLevel4Type0.text=Your defiance is futile. We shall crush you and take what belongs to us. +greetingCLANLevel4Type1.text=You are but a shadow before us. We will strike you down and claim what we desire. +greetingCLANLevel4Type2.text=You stand in our path, but your resistance is meaningless. We shall take what is ours. +greetingCLANLevel5Type0.text=Since you have proven yourself unworthy of honor, we will dispense with a formal Batchall. \ No newline at end of file diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 12116d680e..b5e7489a69 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -21,23 +21,6 @@ */ package mekhq.campaign; -import static mekhq.campaign.personnel.backgrounds.BackgroundsController.randomMercenaryCompanyNameGenerator; -import static mekhq.campaign.personnel.education.EducationController.getAcademy; -import static mekhq.campaign.personnel.turnoverAndRetention.RetirementDefectionTracker.Payout.isBreakingContract; -import static mekhq.campaign.unit.Unit.SITE_FACILITY_MAINTENANCE; - -import java.io.PrintWriter; -import java.text.MessageFormat; -import java.time.DayOfWeek; -import java.time.LocalDate; -import java.time.Month; -import java.time.temporal.ChronoUnit; -import java.util.*; -import java.util.Map.Entry; -import java.util.stream.Collectors; - -import javax.swing.JOptionPane; - import megamek.client.generator.RandomGenderGenerator; import megamek.client.generator.RandomNameGenerator; import megamek.client.generator.RandomUnitGenerator; @@ -54,11 +37,7 @@ import megamek.common.loaders.BLKFile; import megamek.common.loaders.EntityLoadingException; import megamek.common.loaders.EntitySavingException; -import megamek.common.options.GameOptions; -import megamek.common.options.IBasicOption; -import megamek.common.options.IOption; -import megamek.common.options.IOptionGroup; -import megamek.common.options.OptionsConstants; +import megamek.common.options.*; import megamek.common.util.BuildingBlock; import megamek.common.weapons.autocannons.ACWeapon; import megamek.common.weapons.flamers.FlamerWeapon; @@ -71,11 +50,7 @@ import mekhq.campaign.Quartermaster.PartAcquisitionResult; import mekhq.campaign.againstTheBot.AtBConfiguration; import mekhq.campaign.event.*; -import mekhq.campaign.finances.Accountant; -import mekhq.campaign.finances.CurrencyManager; -import mekhq.campaign.finances.Finances; -import mekhq.campaign.finances.Loan; -import mekhq.campaign.finances.Money; +import mekhq.campaign.finances.*; import mekhq.campaign.finances.enums.TransactionType; import mekhq.campaign.force.Force; import mekhq.campaign.force.Lance; @@ -90,12 +65,7 @@ import mekhq.campaign.market.ShoppingList; import mekhq.campaign.market.unitMarket.AbstractUnitMarket; import mekhq.campaign.market.unitMarket.DisabledUnitMarket; -import mekhq.campaign.mission.AtBContract; -import mekhq.campaign.mission.AtBDynamicScenario; -import mekhq.campaign.mission.AtBScenario; -import mekhq.campaign.mission.Contract; -import mekhq.campaign.mission.Mission; -import mekhq.campaign.mission.Scenario; +import mekhq.campaign.mission.*; import mekhq.campaign.mission.atb.AtBScenarioFactory; import mekhq.campaign.mission.enums.AtBLanceRole; import mekhq.campaign.mission.enums.MissionStatus; @@ -105,12 +75,7 @@ import mekhq.campaign.parts.equipment.AmmoBin; import mekhq.campaign.parts.equipment.EquipmentPart; import mekhq.campaign.parts.equipment.MissingEquipmentPart; -import mekhq.campaign.personnel.Bloodname; -import mekhq.campaign.personnel.Person; -import mekhq.campaign.personnel.PersonnelOptions; -import mekhq.campaign.personnel.Skill; -import mekhq.campaign.personnel.SkillType; -import mekhq.campaign.personnel.SpecialAbility; +import mekhq.campaign.personnel.*; import mekhq.campaign.personnel.autoAwards.AutoAwardsController; import mekhq.campaign.personnel.death.AbstractDeath; import mekhq.campaign.personnel.death.DisabledRandomDeath; @@ -118,12 +83,7 @@ import mekhq.campaign.personnel.divorce.DisabledRandomDivorce; import mekhq.campaign.personnel.education.Academy; import mekhq.campaign.personnel.education.EducationController; -import mekhq.campaign.personnel.enums.FamilialRelationshipType; -import mekhq.campaign.personnel.enums.PersonnelRole; -import mekhq.campaign.personnel.enums.PersonnelStatus; -import mekhq.campaign.personnel.enums.Phenotype; -import mekhq.campaign.personnel.enums.PrisonerStatus; -import mekhq.campaign.personnel.enums.SplittingSurnameStyle; +import mekhq.campaign.personnel.enums.*; import mekhq.campaign.personnel.generator.AbstractPersonnelGenerator; import mekhq.campaign.personnel.generator.DefaultPersonnelGenerator; import mekhq.campaign.personnel.generator.RandomPortraitGenerator; @@ -136,26 +96,23 @@ import mekhq.campaign.personnel.ranks.Ranks; import mekhq.campaign.personnel.turnoverAndRetention.Fatigue; import mekhq.campaign.personnel.turnoverAndRetention.RetirementDefectionTracker; +import mekhq.campaign.rating.CamOpsReputation.ReputationController; import mekhq.campaign.rating.FieldManualMercRevDragoonsRating; import mekhq.campaign.rating.IUnitRating; import mekhq.campaign.rating.UnitRatingMethod; -import mekhq.campaign.rating.CamOpsReputation.ReputationController; import mekhq.campaign.storyarc.StoryArc; import mekhq.campaign.stratcon.StratconContractInitializer; import mekhq.campaign.stratcon.StratconRulesManager; import mekhq.campaign.stratcon.StratconTrackState; -import mekhq.campaign.unit.CargoStatistics; import mekhq.campaign.unit.CrewType; -import mekhq.campaign.unit.HangarStatistics; -import mekhq.campaign.unit.TestUnit; -import mekhq.campaign.unit.Unit; -import mekhq.campaign.unit.UnitOrder; -import mekhq.campaign.unit.UnitTechProgression; +import mekhq.campaign.unit.*; import mekhq.campaign.universe.*; import mekhq.campaign.universe.Planet.PlanetaryEvent; import mekhq.campaign.universe.PlanetarySystem.PlanetarySystemEvent; import mekhq.campaign.universe.eras.Era; import mekhq.campaign.universe.eras.Eras; +import mekhq.campaign.universe.fameAndInfamy.BatchallFactions; +import mekhq.campaign.universe.fameAndInfamy.FameAndInfamyController; import mekhq.campaign.universe.selectors.factionSelectors.AbstractFactionSelector; import mekhq.campaign.universe.selectors.factionSelectors.DefaultFactionSelector; import mekhq.campaign.universe.selectors.factionSelectors.RangedFactionSelector; @@ -171,6 +128,22 @@ import mekhq.service.mrms.MRMSService; import mekhq.utilities.MHQXMLUtility; +import javax.swing.*; +import java.io.PrintWriter; +import java.text.MessageFormat; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.Month; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +import static mekhq.campaign.personnel.backgrounds.BackgroundsController.randomMercenaryCompanyNameGenerator; +import static mekhq.campaign.personnel.education.EducationController.getAcademy; +import static mekhq.campaign.personnel.turnoverAndRetention.RetirementDefectionTracker.Payout.isBreakingContract; +import static mekhq.campaign.unit.Unit.SITE_FACILITY_MAINTENANCE; + /** * The main campaign class, keeps track of teams and units * @@ -293,6 +266,7 @@ public class Campaign implements ITechManager { private final CampaignSummary campaignSummary; private final Quartermaster quartermaster; private StoryArc storyArc; + private FameAndInfamyController fameAndInfamy; private final transient ResourceBundle resources = ResourceBundle.getBundle("mekhq.resources.Campaign", MekHQ.getMHQOptions().getLocale()); @@ -359,6 +333,7 @@ public Campaign() { campaignSummary = new CampaignSummary(this); quartermaster = new Quartermaster(this); fieldKitchenWithinCapacity = false; + fameAndInfamy = new FameAndInfamyController(); } /** @@ -3609,12 +3584,22 @@ && getCampaignOptions().getRandomDependentMethod().isAgainstTheBot() } for (AtBContract contract : getActiveAtBContracts()) { - contract.checkMorale(getLocalDate(), getAtBUnitRatingMod()); + contract.checkMorale(this, getLocalDate(), getAtBUnitRatingMod()); addReport("Enemy Morale is now " + contract.getMoraleLevel() + " on contract " + contract.getName()); } } + for (AtBContract contract : getActiveAtBContracts()) { + if (campaignOptions.isUseGenericBattleValue()) { + if (contract.getStartDate().equals(getLocalDate()) && getLocation().isOnPlanet()) { + if (BatchallFactions.usesBatchalls(contract.getEnemyCode())) { + contract.setBatchallAccepted(contract.initiateBatchall(this)); + } + } + } + } + processNewDayATBScenarios(); } @@ -4829,6 +4814,10 @@ public List getCurrentObjectives() { return new ArrayList<>(); } + public FameAndInfamyController getFameAndInfamy() { + return fameAndInfamy; + } + public void writeToXML(final PrintWriter pw) { int indent = 0; @@ -4966,6 +4955,11 @@ public void writeToXML(final PrintWriter pw) { storyArc.writeToXml(pw, indent); } + // Fame and Infamy + if (fameAndInfamy != null) { + fameAndInfamy.writeToXml(pw, indent); + } + // Markets getPersonnelMarket().writeToXML(pw, indent, this); diff --git a/MekHQ/src/mekhq/campaign/CampaignOptions.java b/MekHQ/src/mekhq/campaign/CampaignOptions.java index 7dcb4f182a..021c160ec4 100644 --- a/MekHQ/src/mekhq/campaign/CampaignOptions.java +++ b/MekHQ/src/mekhq/campaign/CampaignOptions.java @@ -19,17 +19,6 @@ */ package mekhq.campaign; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - import megamek.Version; import megamek.codeUtilities.MathUtility; import megamek.common.EquipmentType; @@ -49,6 +38,12 @@ import mekhq.campaign.rating.UnitRatingMethod; import mekhq.service.mrms.MRMSOption; import mekhq.utilities.MHQXMLUtility; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.io.PrintWriter; +import java.util.*; +import java.util.Map.Entry; /** * @author natit @@ -571,6 +566,8 @@ public static String getTransitUnitName(final int unit) { private boolean generateChases; // Scenarios + private boolean useGenericBattleValue; + private boolean useVerboseBidding; private boolean doubleVehicles; private int opForLanceTypeMeks; private int opForLanceTypeMixed; @@ -1205,6 +1202,8 @@ public CampaignOptions() { generateChases = true; // Scenarios + useGenericBattleValue = true; + useVerboseBidding = false; doubleVehicles = false; setOpForLanceTypeMeks(1); setOpForLanceTypeMixed(2); @@ -4259,6 +4258,43 @@ public void setClanVehicles(final boolean clanVehicles) { this.clanVehicles = clanVehicles; } + /** + * Returns whether Generic BV is being used. + * + * @return {@code true} if Generic BV is enabled, {@code false} otherwise. + */ + public boolean isUseGenericBattleValue() { + return useGenericBattleValue; + } + + + /** + * Sets the flag indicating whether BV Balanced bot forces should use Generic BV. + * + * @param useGenericBattleValue flag indicating whether to use Generic BV + */ + public void setUseGenericBattleValue(final boolean useGenericBattleValue) { + this.useGenericBattleValue = useGenericBattleValue; + } + + /** + * Returns whether the verbose bidding mode is enabled. + * + * @return {@code true} if verbose bidding is enabled, {@code false} otherwise. + */ + public boolean isUseVerboseBidding() { + return useVerboseBidding; + } + + /** + * Sets the flag indicating whether verbose bidding should be used. + * + * @param useVerboseBidding flag indicating whether to use verbose bidding + */ + public void setUseVerboseBidding(final boolean useVerboseBidding) { + this.useVerboseBidding = useVerboseBidding; + } + public boolean isDoubleVehicles() { return doubleVehicles; } @@ -5091,6 +5127,8 @@ public void writeToXml(final PrintWriter pw, int indent) { MHQXMLUtility.writeSimpleXMLTag(pw, indent, "useAero", useAero); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "useVehicles", useVehicles); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "clanVehicles", clanVehicles); + MHQXMLUtility.writeSimpleXMLTag(pw, indent, "useGenericBattleValue", useGenericBattleValue); + MHQXMLUtility.writeSimpleXMLTag(pw, indent, "useVerboseBidding", useVerboseBidding); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "doubleVehicles", doubleVehicles); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "adjustPlayerVehicles", adjustPlayerVehicles); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "opForLanceTypeMeks", getOpForLanceTypeMeks()); @@ -6069,6 +6107,10 @@ public static CampaignOptions generateCampaignOptionsFromXml(Node wn, Version ve retVal.useVehicles = Boolean.parseBoolean(wn2.getTextContent().trim()); } else if (wn2.getNodeName().equalsIgnoreCase("clanVehicles")) { retVal.clanVehicles = Boolean.parseBoolean(wn2.getTextContent().trim()); + } else if (wn2.getNodeName().equalsIgnoreCase("useGenericBattleValue")) { + retVal.useGenericBattleValue = Boolean.parseBoolean(wn2.getTextContent().trim()); + } else if (wn2.getNodeName().equalsIgnoreCase("useVerboseBidding")) { + retVal.useVerboseBidding = Boolean.parseBoolean(wn2.getTextContent().trim()); } else if (wn2.getNodeName().equalsIgnoreCase("doubleVehicles")) { retVal.doubleVehicles = Boolean.parseBoolean(wn2.getTextContent().trim()); } else if (wn2.getNodeName().equalsIgnoreCase("adjustPlayerVehicles")) { diff --git a/MekHQ/src/mekhq/campaign/againstTheBot/AtBConfiguration.java b/MekHQ/src/mekhq/campaign/againstTheBot/AtBConfiguration.java index 83dd62539a..225119ece7 100644 --- a/MekHQ/src/mekhq/campaign/againstTheBot/AtBConfiguration.java +++ b/MekHQ/src/mekhq/campaign/againstTheBot/AtBConfiguration.java @@ -21,34 +21,10 @@ */ package mekhq.campaign.againstTheBot; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.InputStream; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.List; -import java.util.ResourceBundle; -import java.util.function.Function; - -import javax.xml.parsers.DocumentBuilder; - -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import megamek.common.Compute; -import megamek.common.EntityWeightClass; -import megamek.common.MekSummary; -import megamek.common.MekSummaryCache; -import megamek.common.TargetRoll; -import megamek.common.UnitType; +import megamek.common.*; import megamek.common.annotations.Nullable; import megamek.logging.MMLogger; import mekhq.MekHQ; -import mekhq.utilities.MHQXMLUtility; import mekhq.campaign.Campaign; import mekhq.campaign.finances.Money; import mekhq.campaign.personnel.Person; @@ -57,6 +33,18 @@ import mekhq.campaign.rating.IUnitRating; import mekhq.campaign.universe.Faction; import mekhq.utilities.MHQXMLUtility; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import javax.xml.parsers.DocumentBuilder; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.time.LocalDate; +import java.util.*; +import java.util.function.Function; /** * @author Neoancient @@ -235,26 +223,45 @@ public int weightClassIndex(String wc) { return selectBotLances(org, weightClass, 0f); } + /** + * Selects a bot lance based on the organization, weight class, and roll modifier. + * + * @param org The organization of the bot force tables. + * @param weightClass The weight class of the bot. + * @param rollMod A modifier to the die roll, expressed as a fraction of the total weight. + * @return The selected bot lance, or null if the organization's bot force tables are not found or invalid. + */ public @Nullable String selectBotLances(String org, int weightClass, float rollMod) { - if (botForceTables.containsKey(org)) { - final List> botForceTable = botForceTables.get(org); - int weightClassIndex = weightClassIndex(weightClass); - WeightedTable table; - if ((weightClassIndex < 0) || (weightClassIndex >= botForceTable.size())) { - logger.error(String.format( - "Bot force tables for organization \"%s\" don't have an entry for weight class %d, limiting to valid values", - org, weightClass)); - weightClassIndex = Math.max(0, Math.min(weightClassIndex, botForceTable.size() - 1)); - } - table = botForceTable.get(weightClassIndex); - if (null == table) { - table = getDefaultForceTable("botForce." + org, weightClassIndex); - } - return table.select(rollMod); - } else { + // Check if the bot force tables contain the required organization + if (!botForceTables.containsKey(org)) { logger.error(String.format("Bot force tables for organization \"%s\" not found, ignoring", org)); return null; } + + // Retrieve botForceTable for the organization + final List> botForceTable = botForceTables.get(org); + + // Weight Class Index + int weightClassIndex = weightClassIndex(weightClass); + + // Check if the weightClassIndex is within valid range + if (weightClassIndex < 0 || weightClassIndex >= botForceTable.size()) { + logger.error(String.format("Bot force tables for organization \"%s\" don't have an entry for weight class %d, limiting to valid values", org, weightClass)); + + // Limit the weightClassIndex within valid range + weightClassIndex = Math.max(0, Math.min(weightClassIndex, botForceTable.size() - 1)); + } + + // Fetch table for the weight class + WeightedTable table = botForceTable.get(weightClassIndex); + + // If there isn't relevant table, provide a default one + if (table == null) { + table = getDefaultForceTable("botForce." + org, weightClassIndex); + } + + // Return the selected table + return table.select(rollMod); } public @Nullable String selectBotUnitWeights(String org, int weightClass) { diff --git a/MekHQ/src/mekhq/campaign/force/Force.java b/MekHQ/src/mekhq/campaign/force/Force.java index aee597802b..e0b213a5b1 100644 --- a/MekHQ/src/mekhq/campaign/force/Force.java +++ b/MekHQ/src/mekhq/campaign/force/Force.java @@ -21,23 +21,8 @@ */ package mekhq.campaign.force; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Enumeration; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.TreeMap; -import java.util.UUID; -import java.util.Vector; -import java.util.stream.Collectors; - -import org.w3c.dom.NamedNodeMap; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - import megamek.Version; +import megamek.common.Entity; import megamek.common.annotations.Nullable; import megamek.common.icons.Camouflage; import megamek.logging.MMLogger; @@ -54,6 +39,15 @@ import mekhq.campaign.personnel.Person; import mekhq.campaign.unit.Unit; import mekhq.utilities.MHQXMLUtility; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.io.PrintWriter; +import java.util.*; +import java.util.stream.Collectors; + +import static java.lang.Math.round; /** * This is a hierarchical object to define forces for TO&E. Each Force @@ -576,8 +570,7 @@ public List updateForceIconOperationalStatus( // to get // the ordinal of the force's status. Then assign the operational status to // this. - final int index = (int) Math - .round(statuses.stream().mapToInt(Enum::ordinal).sum() / (statuses.size() * 1.0)); + final int index = (int) round(statuses.stream().mapToInt(Enum::ordinal).sum() / (statuses.size() * 1.0)); final LayeredForceIconOperationalStatus status = LayeredForceIconOperationalStatus.values()[index]; ((LayeredForceIcon) getForceIcon()).getPieces().put(LayeredForceIconLayer.SPECIAL_MODIFIER, new ArrayList<>()); @@ -748,29 +741,73 @@ public int hashCode() { /** * Calculates the force's total BV, including sub forces. * - * @param c The working campaign. - * @return Total BV + * @param campaign The working campaign. This is the campaign object that the force belongs to. + * @param forceStandardBattleValue Flag indicating whether to override campaign settings that + * call for the use of Generic BV + * @return The total battle value (BV) of the force. */ - public int getTotalBV(Campaign c) { + public int getTotalBV(Campaign campaign, boolean forceStandardBattleValue) { int bvTotal = 0; - for (Force sforce : getSubForces()) { - bvTotal += sforce.getTotalBV(c); + for (Force subforce : getSubForces()) { + bvTotal += subforce.getTotalBV(campaign, forceStandardBattleValue); } - for (UUID id : getUnits()) { + for (UUID unitId : getUnits()) { // no idea how this would happen, but sometimes a unit in a forces unit ID list // has an invalid ID? - if (c.getUnit(id) == null) { + if (campaign.getUnit(unitId) == null) { continue; } - bvTotal += c.getUnit(id).getEntity().calculateBattleValue(); + if (campaign.getCampaignOptions().isUseGenericBattleValue() && !forceStandardBattleValue) { + bvTotal += campaign.getUnit(unitId).getEntity().getGenericBattleValue(); + } else { + bvTotal += campaign.getUnit(unitId).getEntity().calculateBattleValue(); + } } return bvTotal; } + /** + * Calculates the total count of units in the given force, including all sub forces. + * + * @param campaign the current campaign + * @param isClanBidding flag to indicate whether clan bidding is being performed + * @return the total count of units in the force (including sub forces) + */ + public int getTotalUnitCount(Campaign campaign, boolean isClanBidding) { + int unitTotal = 0; + + for (Force subforce : getSubForces()) { + unitTotal += subforce.getTotalUnitCount(campaign, isClanBidding); + } + + // If we're getting the unit count specifically for Clan Bidding, we don't want to count + // Conventional Infantry, and we only count Battle Armor as half a unit. + // If we're not performing Clan Bidding, we just need the total count of units. + if (isClanBidding) { + double rollingCount = 0; + + for (UUID unitId : getUnits()) { + Entity unit = campaign.getUnit(unitId).getEntity(); + + if (unit.isBattleArmor()) { + rollingCount += 0.5; + } else if (!unit.isConventionalInfantry()) { + rollingCount++; + } + } + + unitTotal += (int) round(rollingCount); + } else { + unitTotal += getUnits().size(); + } + + return unitTotal; + } + /** * Calculates the unit type most represented in this force * and all subforces. diff --git a/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java b/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java index b0333ec883..0bbc8daf25 100644 --- a/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java +++ b/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java @@ -18,44 +18,12 @@ */ package mekhq.campaign.io; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintStream; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.UUID; - -import javax.xml.parsers.DocumentBuilder; - -import org.apache.commons.lang3.StringUtils; -import org.w3c.dom.DOMException; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - import megamek.Version; import megamek.client.generator.RandomGenderGenerator; import megamek.client.generator.RandomNameGenerator; import megamek.client.ui.swing.util.PlayerColour; import megamek.common.Entity; -import megamek.common.EntityMovementMode; -import megamek.common.Jumpship; -import megamek.common.Mek; -import megamek.common.MekSummaryCache; -import megamek.common.MiscType; -import megamek.common.Mounted; -import megamek.common.SmallCraft; -import megamek.common.Tank; +import megamek.common.*; import megamek.common.annotations.Nullable; import megamek.common.icons.Camouflage; import megamek.common.weapons.bayweapons.BayWeapon; @@ -63,12 +31,7 @@ import mekhq.MekHQ; import mekhq.NullEntityException; import mekhq.Utilities; -import mekhq.campaign.Campaign; -import mekhq.campaign.CampaignOptions; -import mekhq.campaign.CurrentLocation; -import mekhq.campaign.Kill; -import mekhq.campaign.RandomSkillPreferences; -import mekhq.campaign.Warehouse; +import mekhq.campaign.*; import mekhq.campaign.againstTheBot.AtBConfiguration; import mekhq.campaign.finances.Finances; import mekhq.campaign.force.Force; @@ -81,20 +44,8 @@ import mekhq.campaign.mission.Mission; import mekhq.campaign.mission.Scenario; import mekhq.campaign.mod.am.InjuryTypes; -import mekhq.campaign.parts.EnginePart; -import mekhq.campaign.parts.MekActuator; -import mekhq.campaign.parts.MekLocation; -import mekhq.campaign.parts.MissingEnginePart; -import mekhq.campaign.parts.MissingMekActuator; -import mekhq.campaign.parts.MissingPart; -import mekhq.campaign.parts.Part; -import mekhq.campaign.parts.equipment.AmmoBin; -import mekhq.campaign.parts.equipment.EquipmentPart; -import mekhq.campaign.parts.equipment.HeatSink; -import mekhq.campaign.parts.equipment.MASC; -import mekhq.campaign.parts.equipment.MissingAmmoBin; -import mekhq.campaign.parts.equipment.MissingEquipmentPart; -import mekhq.campaign.parts.equipment.MissingMASC; +import mekhq.campaign.parts.*; +import mekhq.campaign.parts.equipment.*; import mekhq.campaign.personnel.Person; import mekhq.campaign.personnel.PersonnelOptions; import mekhq.campaign.personnel.SkillType; @@ -112,9 +63,18 @@ import mekhq.campaign.universe.Planet.PlanetaryEvent; import mekhq.campaign.universe.PlanetarySystem.PlanetarySystemEvent; import mekhq.campaign.universe.Systems; +import mekhq.campaign.universe.fameAndInfamy.FameAndInfamyController; import mekhq.io.idReferenceClasses.PersonIdReference; import mekhq.module.atb.AtBEventProcessor; import mekhq.utilities.MHQXMLUtility; +import org.apache.commons.lang3.StringUtils; +import org.w3c.dom.*; + +import javax.xml.parsers.DocumentBuilder; +import java.io.*; +import java.time.LocalDate; +import java.util.*; +import java.util.Map.Entry; public class CampaignXmlParser { private static final MMLogger logger = MMLogger.create(CampaignXmlParser.class); @@ -298,6 +258,8 @@ public Campaign parse() throws CampaignXmlParseException, NullEntityException { processSpecialAbilityNodes(retVal, wn, version); } else if (xn.equalsIgnoreCase("storyArc")) { processStoryArcNodes(retVal, wn, version); + } else if (xn.equalsIgnoreCase("fameAndInfamy")) { + processFameAndInfamyNodes(retVal, wn); } else if (xn.equalsIgnoreCase("gameOptions")) { retVal.getGameOptions().fillFromXML(wn.getChildNodes()); } else if (xn.equalsIgnoreCase("kills")) { @@ -957,6 +919,11 @@ private static void processStoryArcNodes(Campaign retVal, Node wn, Version versi retVal.useStoryArc(storyArc, false); } + private static void processFameAndInfamyNodes(Campaign relativeValue, Node workingNode) { + logger.info("Loading Fame and Infamy Nodes from XML..."); + FameAndInfamyController.parseFromXML(workingNode.getChildNodes(), relativeValue); + } + private static void processSpecialAbilityNodes(Campaign retVal, Node wn, Version version) { logger.info("Loading Special Ability Nodes from XML..."); diff --git a/MekHQ/src/mekhq/campaign/mission/AtBContract.java b/MekHQ/src/mekhq/campaign/mission/AtBContract.java index 897e2beedf..c9d8aa5810 100644 --- a/MekHQ/src/mekhq/campaign/mission/AtBContract.java +++ b/MekHQ/src/mekhq/campaign/mission/AtBContract.java @@ -21,25 +21,12 @@ */ package mekhq.campaign.mission; -import java.io.PrintWriter; -import java.text.ParseException; -import java.time.DayOfWeek; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.Objects; -import java.util.UUID; - -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - +import megamek.client.generator.RandomNameGenerator; import megamek.client.generator.RandomUnitGenerator; import megamek.client.ui.swing.util.PlayerColour; -import megamek.common.Compute; -import megamek.common.Entity; -import megamek.common.MekFileParser; -import megamek.common.MekSummary; -import megamek.common.UnitType; +import megamek.common.*; import megamek.common.annotations.Nullable; +import megamek.common.enums.Gender; import megamek.common.enums.SkillLevel; import megamek.common.icons.Camouflage; import megamek.common.loaders.EntityLoadingException; @@ -52,9 +39,11 @@ import mekhq.campaign.mission.atb.AtBScenarioFactory; import mekhq.campaign.mission.enums.AtBContractType; import mekhq.campaign.mission.enums.AtBMoraleLevel; +import mekhq.campaign.personnel.Bloodname; import mekhq.campaign.personnel.Person; import mekhq.campaign.personnel.SkillType; import mekhq.campaign.personnel.backgrounds.BackgroundsController; +import mekhq.campaign.personnel.enums.Phenotype; import mekhq.campaign.rating.IUnitRating; import mekhq.campaign.stratcon.StratconCampaignState; import mekhq.campaign.stratcon.StratconContractDefinition; @@ -63,7 +52,21 @@ import mekhq.campaign.universe.Faction; import mekhq.campaign.universe.Factions; import mekhq.campaign.universe.RandomFactionGenerator; +import mekhq.campaign.universe.fameAndInfamy.BatchallFactions; import mekhq.utilities.MHQXMLUtility; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import javax.swing.*; +import java.awt.*; +import java.io.PrintWriter; +import java.text.ParseException; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Objects; +import java.util.ResourceBundle; +import java.util.UUID; /** * Contract class for use with Against the Bot rules @@ -117,6 +120,7 @@ public class AtBContract extends Contract { protected LocalDate routEnd; protected int partsAvailabilityLevel; protected int sharesPct; + private boolean batchallAccepted; protected int playerMinorBreaches; protected int employerMinorBreaches; @@ -141,6 +145,10 @@ public class AtBContract extends Contract { private StratconCampaignState stratconCampaignState; + private static final ResourceBundle resources = ResourceBundle.getBundle( + "mekhq.resources.AtBContract", + MekHQ.getMHQOptions().getLocale()); + protected AtBContract() { this(null); } @@ -169,6 +177,7 @@ public AtBContract(String name) { extensionLength = 0; sharesPct = 0; + batchallAccepted = true; setMoraleLevel(AtBMoraleLevel.NORMAL); routEnd = null; numBonusParts = 0; @@ -290,12 +299,19 @@ public void calculatePaymentMultiplier(Campaign campaign) { setMultiplier(multiplier); } - public void checkMorale(LocalDate today, int dragoonRating) { + /** + * Checks the morale level of the campaign based on various factors. + * + * @param campaign The ongoing campaign. + * @param today The current date. + * @param dragoonRating The player's dragoon rating + */ + public void checkMorale(Campaign campaign, LocalDate today, int dragoonRating) { if (null != routEnd) { if (today.isAfter(routEnd)) { setMoraleLevel(AtBMoraleLevel.NORMAL); routEnd = null; - updateEnemy(today); // mix it up a little + updateEnemy(campaign, today); // mix it up a little } else { setMoraleLevel(AtBMoraleLevel.BROKEN); } @@ -393,10 +409,12 @@ public void checkMorale(LocalDate today, int dragoonRating) { } /** - * Changes the enemy to a randomly selected faction that's an enemy of - * the current employer + * Updates the enemy faction and enemy bot name for this contract. + * + * @param campaign The current campaign. + * @param today The current LocalDate object. */ - private void updateEnemy(LocalDate today) { + private void updateEnemy(Campaign campaign, LocalDate today) { String enemyCode = RandomFactionGenerator.getInstance().getEnemy( Factions.getInstance().getFaction(employerCode), false, true); setEnemyCode(enemyCode); @@ -405,6 +423,14 @@ private void updateEnemy(LocalDate today) { setEnemyBotName(enemyFaction.getFullName(today.getYear())); enemyName = ""; // wipe the old enemy name getEnemyName(today.getYear()); // we use this to update enemyName + + // Update the Batchall information + batchallAccepted = true; + if (campaign.getCampaignOptions().isUseGenericBattleValue()) { + if (getEnemy().isClan()) { + setBatchallAccepted(initiateBatchall(campaign)); + } + } } /** @@ -844,6 +870,7 @@ protected int writeToXMLBegin(final PrintWriter pw, int indent) { MHQXMLUtility.writeSimpleXMLTag(pw, indent, "partsAvailabilityLevel", getPartsAvailabilityLevel()); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "extensionLength", extensionLength); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "sharesPct", sharesPct); + MHQXMLUtility.writeSimpleXMLTag(pw, indent, "batchallAccepted", batchallAccepted); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "playerMinorBreaches", playerMinorBreaches); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "employerMinorBreaches", employerMinorBreaches); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "contractScoreArbitraryModifier", contractScoreArbitraryModifier); @@ -920,6 +947,8 @@ public void loadFieldsFromXmlNode(Node wn) throws ParseException { extensionLength = Integer.parseInt(wn2.getTextContent()); } else if (wn2.getNodeName().equalsIgnoreCase("sharesPct")) { sharesPct = Integer.parseInt(wn2.getTextContent()); + } else if (wn2.getNodeName().equalsIgnoreCase("batchallAccepted")) { + batchallAccepted = Boolean.parseBoolean(wn2.getTextContent()); } else if (wn2.getNodeName().equalsIgnoreCase("numBonusParts")) { numBonusParts = Integer.parseInt(wn2.getTextContent()); } else if (wn2.getNodeName().equalsIgnoreCase("playerMinorBreaches")) { @@ -1184,6 +1213,24 @@ public void setAtBSharesPercent(int pct) { sharesPct = pct; } + /** + * Checks if the Batchall has been accepted for the contract. + * + * @return {@code true} if the Batchall has been accepted, {@code false} otherwise. + */ + public boolean isBatchallAccepted() { + return batchallAccepted; + } + + /** + * Sets the {@code batchallAccepted} flag for this contract. + * + * @param batchallAccepted The value to set for the {@code batchallAccepted} flag. + */ + public void setBatchallAccepted(final boolean batchallAccepted) { + this.batchallAccepted = batchallAccepted; + } + public void addPlayerMinorBreach() { playerMinorBreaches++; } @@ -1323,4 +1370,273 @@ public AtBContractRef(int id) { setId(id); } } + + /** + * This method initiates a batchall, a challenge/dialog to decide on the conduct of a campaign. + * Prompts the player with a message and options to accept or refuse the batchall. + * + * @param campaign The current campaign. + * @return {@code true} if the batchall is accepted, {@code false} otherwise. + */ + // + public boolean initiateBatchall(Campaign campaign) { + // Retrieves the title from the resources + String title = resources.getString("incomingTransmission.title"); + + // Retrieves the batchall statement based on infamy and enemy code + String batchallStatement = BatchallFactions.getGreeting(campaign, enemyCode); + + // Constants for the directory of the portraits and the file type + final String PORTRAIT_DIRECTORY = "data/images/force/Pieces/Logos/Clan/"; + final String PORTRAIT_FILE_TYPE = ".png"; + + // An ImageIcon to hold the clan's faction icon + ImageIcon icon; + + // A switch statement that selects the icon based on the enemy code + switch (enemyCode) { + // Each case sets the icon to the corresponding image + case "CBS" -> icon = new ImageIcon(PORTRAIT_DIRECTORY + "Clan Blood Spirit" + + PORTRAIT_FILE_TYPE); + case "CB" -> icon = new ImageIcon(PORTRAIT_DIRECTORY + "Clan Burrock" + + PORTRAIT_FILE_TYPE); + case "CCC" -> icon = new ImageIcon(PORTRAIT_DIRECTORY + "Clan Cloud Cobra" + + PORTRAIT_FILE_TYPE); + case "CCO" -> icon = new ImageIcon(PORTRAIT_DIRECTORY + "Clan Coyote" + + PORTRAIT_FILE_TYPE); + case "CDS" -> { + if (campaign.getGameYear() >= 3100) { + icon = new ImageIcon(PORTRAIT_DIRECTORY + "Clan Sea Fox" + + PORTRAIT_FILE_TYPE); + } else { + icon = new ImageIcon(PORTRAIT_DIRECTORY + "Clan Diamond Shark" + + PORTRAIT_FILE_TYPE); + } + } + case "CFM" -> icon = new ImageIcon(PORTRAIT_DIRECTORY + "Clan Fire Mandrill" + + PORTRAIT_FILE_TYPE); + case "CGB" -> { + if (campaign.getGameYear() >= 3060) { + icon = new ImageIcon(PORTRAIT_DIRECTORY + "Ghost Bear Dominion" + + PORTRAIT_FILE_TYPE); + } else { + icon = new ImageIcon(PORTRAIT_DIRECTORY + "Clan Ghost Bear" + + PORTRAIT_FILE_TYPE); + } + } + case "CGS" -> icon = new ImageIcon(PORTRAIT_DIRECTORY + "Clan Goliath Scorpion" + + PORTRAIT_FILE_TYPE); + case "CHH" -> icon = new ImageIcon(PORTRAIT_DIRECTORY + "Clan Hell's Horses" + + PORTRAIT_FILE_TYPE); + case "CIH" -> icon = new ImageIcon(PORTRAIT_DIRECTORY + "Clan Ice Hellion" + + PORTRAIT_FILE_TYPE); + case "CJF" -> icon = new ImageIcon(PORTRAIT_DIRECTORY + "Clan Jade Falcon" + + PORTRAIT_FILE_TYPE); + case "CMG" -> icon = new ImageIcon(PORTRAIT_DIRECTORY + "Clan Mongoose" + + PORTRAIT_FILE_TYPE); + case "CNC" -> icon = new ImageIcon(PORTRAIT_DIRECTORY + "Clan Nova Cat" + + PORTRAIT_FILE_TYPE); + case "CSJ" -> icon = new ImageIcon(PORTRAIT_DIRECTORY + "Clan Smoke Jaguar" + + PORTRAIT_FILE_TYPE); + case "CSR" -> icon = new ImageIcon(PORTRAIT_DIRECTORY + "Clan Snow Raven" + + PORTRAIT_FILE_TYPE); + case "CSA" -> icon = new ImageIcon(PORTRAIT_DIRECTORY + "Clan Star Adder" + + PORTRAIT_FILE_TYPE); + case "CSV" -> icon = new ImageIcon(PORTRAIT_DIRECTORY + "Clan Steel Viper" + + PORTRAIT_FILE_TYPE); + case "CSL" -> icon = new ImageIcon(PORTRAIT_DIRECTORY + "Clan Stone Lion" + + PORTRAIT_FILE_TYPE); + case "CWI" -> icon = new ImageIcon(PORTRAIT_DIRECTORY + "Clan Widowmaker" + + PORTRAIT_FILE_TYPE); + case "CW", "CWE" -> icon = new ImageIcon(PORTRAIT_DIRECTORY + "Clan Wolf" + + PORTRAIT_FILE_TYPE); + case "CWIE" -> icon = new ImageIcon(PORTRAIT_DIRECTORY + "Clan Wolf-in-Exile" + + PORTRAIT_FILE_TYPE); + case "CEI" -> icon = new ImageIcon(PORTRAIT_DIRECTORY + "Scorpion Empire" + + PORTRAIT_FILE_TYPE); + case "RD" -> icon = new ImageIcon(PORTRAIT_DIRECTORY + "Rasalhague Dominion" + + PORTRAIT_FILE_TYPE); + case "RA" -> icon = new ImageIcon(PORTRAIT_DIRECTORY + "Raven Alliance" + + PORTRAIT_FILE_TYPE); + default -> icon = new ImageIcon("data/images/force/Pieces/Logos/Inner Sphere/Star League.png"); + } + + // Set the commander's rank and use a name generator to generate the commander's name + String rank = resources.getString("starColonel.text"); + RandomNameGenerator randomNameGenerator = new RandomNameGenerator(); + String commander = randomNameGenerator.generate(Gender.RANDOMIZE, true, enemyCode); + commander += ' ' + Bloodname.randomBloodname(enemyCode, Phenotype.MEKWARRIOR, + campaign.getGameYear()).getName(); + + // Construct the batchall message + String message = String.format(resources.getString("batchallOpener.text"), + this.getName(), rank, commander, getEnemy().getFullName(campaign.getGameYear()), + getSystemName(campaign.getLocalDate())); + message = message + batchallStatement; + + // Append additional message text if the fame is less than 5 + if (campaign.getFameAndInfamy().getFameForFaction(enemyCode) < 5) { + message = message + resources.getString("batchallCloser.text"); + } + + // Create a text pane to display the message + JTextPane textPane = new JTextPane(); + textPane.setContentType("text/html"); + textPane.setText(message); + textPane.setEditable(false); + + // Create a panel to display the icon and the batchall message + JPanel panel = new JPanel(new BorderLayout()); + JLabel imageLabel = new JLabel(icon); + panel.add(imageLabel, BorderLayout.CENTER); + panel.add(textPane, BorderLayout.SOUTH); + + // Choose dialog to display based on the fame + if (campaign.getFameAndInfamy().getFameForFaction(enemyCode) > 4) { + noBatchallOfferedDialog(panel, title); + return false; + } else { + return batchallDialog(campaign, panel, title); + } + } + + /** + * This function creates a dialog with accept and refuse buttons. + * + * @param campaign the current campaign + * @param panel the panel to display in the dialog + * @param title the title of the dialog + * @return {@code true} if the batchall is accepted, {@code false} otherwise + */ + private boolean batchallDialog(Campaign campaign, JPanel panel, String title) { + // We use a single-element array to store the result, because we need to modify it inside + // the action listeners, which requires the variable to be effectively final + final boolean[] result = {false}; + + // Create a custom dialog + JDialog dialog = new JDialog(); + dialog.setTitle(title); // Set the title of the dialog + dialog.setLayout(new BorderLayout()); // Set a border layout manager + + // Create an accept button and add its action listener. When clicked, it will set the result + // to true and close the dialog + JButton acceptButton = new JButton(resources.getString("responseAccept.text")); + acceptButton.setToolTipText(resources.getString("responseAccept.tooltip")); + acceptButton.addActionListener(e -> { + result[0] = true; + dialog.dispose(); + }); + + // Create a refuse button and add its action listener. + // When clicked, it will trigger a refusal confirmation dialog + JButton refuseButton = new JButton(resources.getString("responseRefuse.text")); + refuseButton.setToolTipText(resources.getString("responseRefuse.tooltip")); + refuseButton.addActionListener(e -> { + dialog.dispose(); // Close the current dialog + // Use another method to show a refusal confirmation dialog and store the result + result[0] = refusalConfirmationDialog(campaign); + }); + + // Create a panel for buttons and add buttons to it + JPanel buttonPanel = new JPanel(); + buttonPanel.add(acceptButton); + buttonPanel.add(refuseButton); + + // Add the original panel and button panel to the dialog + dialog.add(panel, BorderLayout.CENTER); + dialog.add(buttonPanel, BorderLayout.SOUTH); + + dialog.pack(); // Size the dialog to fit the preferred size and layouts of its components + dialog.setLocationRelativeTo(null); // Center the dialog on the screen + dialog.setModal(true); // Make the dialog block user input to other top-level windows + dialog.setVisible(true); // Show the dialog + + return result[0]; // Return the result when the dialog is disposed + } + + /** + * This function displays a dialog asking for final confirmation to refuse a batchall, + * and performs related actions if the refusal is confirmed. + * + * @param campaign the current campaign + * @return {@code true} if the user accepts the refusal, {@code false} if the user cancels the refusal + */ + private boolean refusalConfirmationDialog(Campaign campaign) { + // Create modal JDialog + JDialog dialog = new JDialog(); + dialog.setLayout(new BorderLayout()); + + // Buffer for storing user response (acceptance/refusal) + final boolean[] response = {false}; + + // "Accept" Button + JButton acceptButton = new JButton(resources.getString("responseAccept.text")); + acceptButton.setToolTipText(resources.getString("responseAccept.tooltip")); + acceptButton.addActionListener(e -> { + response[0] = true; // User has accepted + dialog.dispose(); // Close dialog + }); + + // "Refuse" Button + JButton refuseButton = new JButton(resources.getString("responseRefuse.text")); + refuseButton.setToolTipText(resources.getString("responseRefuse.tooltip")); + refuseButton.addActionListener(e -> { + // Update the campaign state on refusal + campaign.addReport(resources.getString("refusalReport.text")); + campaign.getFameAndInfamy().updateFameForFaction(campaign, enemyCode, -1); + response[0] = false; // User has refused + dialog.dispose(); // Close dialog + }); + + // Panel for hosting buttons + JPanel buttonPanel = new JPanel(); + buttonPanel.add(acceptButton); + buttonPanel.add(refuseButton); + + // Message Label + JLabel messageLabel = new JLabel(String.format(resources.getString("refusalConfirmation.text"), + getEnemy().getFullName(campaign.getGameYear()))); + + // Add Message and Buttons to the dialog + dialog.add(messageLabel, BorderLayout.CENTER); + dialog.add(buttonPanel, BorderLayout.SOUTH); + + // Configure and display dialog + dialog.pack(); // Fit dialog to its contents + dialog.setLocationRelativeTo(null); // Center dialog + dialog.setModal(true); // Block access to other windows + dialog.setVisible(true); // Display dialog + + // Return user response + return response[0]; + } + + /** + * Displays a dialog with a message for when the faction has refused to offer a Batchall due to + * past player refusals. + * + * @param panel The panel to display in the dialog. + * @param title The title of the dialog. + */ + private void noBatchallOfferedDialog(JPanel panel, String title) { + // Create a new JDialog + JDialog dialog = new JDialog(); + dialog.setTitle(title); + dialog.setLayout(new BorderLayout()); + + JButton responseButton = new JButton(resources.getString("responseBringItOn.text")); + responseButton.setToolTipText(resources.getString("responseBringItOn.tooltip")); + responseButton.addActionListener(e -> dialog.dispose()); // Dispose the dialog when the button is clicked + + JPanel buttonPanel = new JPanel(); + buttonPanel.add(responseButton); // Add the button to the panel + + dialog.add(panel, BorderLayout.CENTER); + dialog.add(buttonPanel, BorderLayout.SOUTH); + + dialog.pack(); // Size the dialog to fit the preferred size and layouts of its components + dialog.setLocationRelativeTo(null); // Center the dialog on the screen + dialog.setModal(true); // Set the dialog to be modal + dialog.setVisible(true); // Show the dialog + } } diff --git a/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java b/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java index 88347aec21..95809e00f1 100644 --- a/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java +++ b/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java @@ -18,19 +18,8 @@ */ package mekhq.campaign.mission; -import java.io.File; -import java.time.LocalDate; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - import megamek.client.bot.princess.CardinalEdge; -import megamek.client.generator.RandomGenderGenerator; -import megamek.client.generator.RandomNameGenerator; -import megamek.client.generator.RandomUnitGenerator; -import megamek.client.generator.ReconfigurationParameters; -import megamek.client.generator.TeamLoadOutGenerator; -import megamek.client.generator.enums.SkillGeneratorType; +import megamek.client.generator.*; import megamek.client.generator.skillGenerators.AbstractSkillGenerator; import megamek.client.generator.skillGenerators.StratConSkillGenerator; import megamek.client.ratgenerator.MissionRole; @@ -47,11 +36,11 @@ import megamek.logging.MMLogger; import megamek.utilities.BoardClassifier; import mekhq.MHQConstants; +import mekhq.MekHQ; import mekhq.campaign.Campaign; import mekhq.campaign.CampaignOptions; import mekhq.campaign.againstTheBot.AtBConfiguration; import mekhq.campaign.force.Force; -import mekhq.campaign.force.Lance; import mekhq.campaign.mission.AtBDynamicScenario.BenchedEntityData; import mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment; import mekhq.campaign.mission.ScenarioForceTemplate.ForceGenerationMethod; @@ -65,20 +54,23 @@ import mekhq.campaign.personnel.SkillType; import mekhq.campaign.personnel.enums.Phenotype; import mekhq.campaign.rating.IUnitRating; -import mekhq.campaign.rating.UnitRatingMethod; import mekhq.campaign.stratcon.StratconBiomeManifest; import mekhq.campaign.stratcon.StratconContractInitializer; import mekhq.campaign.unit.Unit; -import mekhq.campaign.universe.Faction; +import mekhq.campaign.universe.*; import mekhq.campaign.universe.Faction.Tag; -import mekhq.campaign.universe.Factions; -import mekhq.campaign.universe.IUnitGenerator; -import mekhq.campaign.universe.Planet; -import mekhq.campaign.universe.PlanetarySystem; -import mekhq.campaign.universe.Systems; -import mekhq.campaign.universe.UnitGeneratorParameters; import mekhq.campaign.universe.enums.EraFlag; +import java.io.File; +import java.time.LocalDate; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static java.lang.Math.round; +import static mekhq.campaign.mission.Scenario.T_GROUND; +import static mekhq.campaign.mission.ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_MIX; + /** * This class handles the creation and substantive manipulation of * AtBDynamicScenarios @@ -103,6 +95,10 @@ public class AtBDynamicScenarioFactory { private static final int REINFORCEMENT_ARRIVAL_SCALE = 30; + private static final ResourceBundle resources = ResourceBundle.getBundle( + "mekhq.resources.AtBDynamicScenarioFactory", + MekHQ.getMHQOptions().getLocale()); + /** * Method that sets some initial scenario parameters from the given template, * prior to force generation and such. @@ -200,13 +196,11 @@ public static void finalizeScenario(AtBDynamicScenario scenario, AtBContract con scenario.getExternalIDLookup().clear(); scenario.getBotUnitTemplates().clear(); - // fix the player force weight class and unit count at the current time. - int playerForceWeightClass = calculatePlayerForceWeightClass(scenario, campaign); - int playerForceUnitCount = calculateEffectiveUnitCount(scenario, campaign); + // fix the force unit count at the current time. + int playerForceUnitCount = calculateEffectiveUnitCount(scenario, campaign, false); - // at this point, only the player forces are present and contributing to BV/unit - // count - int generatedLanceCount = generateForces(scenario, contract, campaign, playerForceWeightClass); + // at this point, only the player forces are present and contributing to BV/unit count + int generatedLanceCount = generateForces(scenario, contract, campaign); // approximate estimate, anyway. scenario.setLanceCount(generatedLanceCount + (playerForceUnitCount / 4)); @@ -239,11 +233,9 @@ public static void finalizeScenario(AtBDynamicScenario scenario, AtBContract con * @param contract The contract on which we're currently working. Used for * skill/quality/planetary info parameters * @param campaign The current campaign - * @param weightClass The average weight class across all forces * @return How many "lances" or other individual units were generated? */ - private static int generateForces(AtBDynamicScenario scenario, AtBContract contract, Campaign campaign, - int weightClass) { + private static int generateForces(AtBDynamicScenario scenario, AtBContract contract, Campaign campaign) { int generatedLanceCount = 0; List forceTemplates = scenario.getTemplate().getAllScenarioForces(); @@ -271,13 +263,15 @@ private static int generateForces(AtBDynamicScenario scenario, AtBContract contr // recalculate effective BV and unit count each time we change levels for (int generationOrder : generationOrders) { List currentForceTemplates = orderedForceTemplates.get(generationOrder); - effectiveBV = calculateEffectiveBV(scenario, campaign); - effectiveUnitCount = calculateEffectiveUnitCount(scenario, campaign); + effectiveBV = calculateEffectiveBV(scenario, campaign, false); + effectiveUnitCount = calculateEffectiveUnitCount(scenario, campaign, false); for (ScenarioForceTemplate forceTemplate : currentForceTemplates) { if (forceTemplate.getGenerationMethod() == ForceGenerationMethod.FixedMUL.ordinal()) { generatedLanceCount += generateFixedForce(scenario, contract, campaign, forceTemplate); } else { + int weightClass = randomForceWeight(); + generatedLanceCount += generateForce(scenario, contract, campaign, effectiveBV, effectiveUnitCount, weightClass, forceTemplate, false); } @@ -410,8 +404,7 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac } break; default: - logger.warn( - String.format("Invalid force alignment %d", forceTemplate.getForceAlignment())); + logger.warn(String.format("Invalid force alignment %d", forceTemplate.getForceAlignment())); } final Faction faction = Factions.getInstance().getFaction(factionCode); @@ -426,25 +419,18 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac // determine generation parameters int forceBV = 0; - double forceMultiplier = getDifficultyMultiplier(campaign); - forceTemplate.setForceMultiplier(forceMultiplier); - int forceBVBudget = (int) (effectiveBV * forceTemplate.getForceMultiplier()); if (isScenarioModifier) { - forceBVBudget = (int) (forceBVBudget * ((double) campaign.getCampaignOptions().getScenarioModBV() / 100) - * forceTemplate.getForceMultiplier()); + forceBVBudget = (int) (forceBVBudget * ((double) campaign.getCampaignOptions().getScenarioModBV() / 100) * forceTemplate.getForceMultiplier()); } int forceUnitBudget = 0; if (forceTemplate.getGenerationMethod() == ForceGenerationMethod.UnitCountScaled.ordinal()) { forceUnitBudget = (int) (effectiveUnitCount * forceTemplate.getForceMultiplier()); - } else if ((forceTemplate.getGenerationMethod() == ForceGenerationMethod.FixedUnitCount.ordinal()) || - (forceTemplate.getGenerationMethod() == ForceGenerationMethod.PlayerOrFixedUnitCount.ordinal())) { - forceUnitBudget = forceTemplate.getFixedUnitCount() == ScenarioForceTemplate.FIXED_UNIT_SIZE_LANCE - ? lanceSize - : forceTemplate.getFixedUnitCount(); + } else if ((forceTemplate.getGenerationMethod() == ForceGenerationMethod.FixedUnitCount.ordinal()) || (forceTemplate.getGenerationMethod() == ForceGenerationMethod.PlayerOrFixedUnitCount.ordinal())) { + forceUnitBudget = forceTemplate.getFixedUnitCount() == ScenarioForceTemplate.FIXED_UNIT_SIZE_LANCE ? lanceSize : forceTemplate.getFixedUnitCount(); } // Conditions parameters - atmospheric pressure, toxic atmosphere, and gravity @@ -457,8 +443,7 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac isLowPressure = true; allowsTanks = false; } else { - mekhq.campaign.universe.Atmosphere specific_atmosphere = contract.getSystem().getPrimaryPlanet() - .getAtmosphere(currentDate); + mekhq.campaign.universe.Atmosphere specific_atmosphere = contract.getSystem().getPrimaryPlanet().getAtmosphere(currentDate); switch (specific_atmosphere) { case TOXICPOISON: case TOXICCAUSTIC: @@ -492,7 +477,7 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac Collection baseRoles = forceTemplate.getRequiredRoles(); if (!baseRoles.isEmpty()) { - if (forceTemplate.getAllowedUnitType() == ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_MIX) { + if (forceTemplate.getAllowedUnitType() == SPECIAL_UNIT_TYPE_ATB_MIX) { requiredRoles.put(UnitType.MEK, new ArrayList<>(baseRoles)); requiredRoles.put(UnitType.TANK, new ArrayList<>(baseRoles)); } else if (forceTemplate.getAllowedUnitType() == ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_AERO_MIX) { @@ -526,20 +511,17 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac } // If the force template is set up for artillery, add the role to all applicable - // unit - // types including the dynamic Mek/vehicle mixed type + // unit types including the dynamic Mek/vehicle mixed type if (forceTemplate.getUseArtillery()) { int artilleryCarriers = forceTemplate.getAllowedUnitType(); - if (artilleryCarriers == ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_MIX - || artilleryCarriers == UnitType.MEK) { + if (artilleryCarriers == SPECIAL_UNIT_TYPE_ATB_MIX || artilleryCarriers == UnitType.MEK) { if (!requiredRoles.containsKey(UnitType.MEK)) { requiredRoles.put(UnitType.MEK, new HashSet<>()); } requiredRoles.get(UnitType.MEK).add((MissionRole.ARTILLERY)); } - if (artilleryCarriers == ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_MIX - || artilleryCarriers == UnitType.TANK) { + if (artilleryCarriers == SPECIAL_UNIT_TYPE_ATB_MIX || artilleryCarriers == UnitType.TANK) { if (!requiredRoles.containsKey(UnitType.TANK)) { requiredRoles.put(UnitType.TANK, new HashSet<>()); } @@ -576,17 +558,12 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac int actualUnitType = forceTemplate.getAllowedUnitType(); // The SPECIAL_UNIT_TYPE_ATB_AERO_MIX value allows for random selection of - // aerospace or - // conventional fighters. Only allow for conventional fighters where this force - // controls - // the system, and where there is an atmosphere. - // Aerospace fighters are added in single flights/points, while conventional - // fighters - // are added in full squadrons (1-3 flights, 2-6 total). - if (isPlanetOwner && - actualUnitType == ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_AERO_MIX && - scenario.getTemplate().mapParameters.getMapLocation() != MapLocation.Space && - scenario.getAtmosphere().isDenserThan(Atmosphere.THIN)) { + // aerospace or conventional fighters. + // Only allow for conventional fighters where this force controls the system, and where + // there is an atmosphere. Aerospace fighters are added in single flights/points, while + // conventional fighters are added in full squadrons (1-3 flights, 2-6 total). + if (isPlanetOwner && actualUnitType == ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_AERO_MIX + && scenario.getTemplate().mapParameters.getMapLocation() != MapLocation.Space && scenario.getAtmosphere().isDenserThan(Atmosphere.THIN)) { actualUnitType = Compute.d6() > 3 ? UnitType.AEROSPACEFIGHTER : UnitType.CONV_FIGHTER; lanceSize = getAeroLanceSize(actualUnitType, isPlanetOwner, factionCode); } else if (actualUnitType == ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_AERO_MIX) { @@ -600,12 +577,9 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac if (currentLanceWeightString == null) { generatedLance = new ArrayList<>(); // Hazardous conditions may prohibit deploying infantry or vehicles - } else if ((actualUnitType == UnitType.INFANTRY && !allowsConvInfantry) || - (actualUnitType == UnitType.TANK && !allowsTanks)) { + } else if ((actualUnitType == UnitType.INFANTRY && !allowsConvInfantry) || (actualUnitType == UnitType.TANK && !allowsTanks)) { generatedLance = new ArrayList<>(); - logger - .warn(String.format("Skipping generation of unit type %s due to hostile conditions.", - UnitType.getTypeName(actualUnitType))); + logger.warn(String.format("Skipping generation of unit type %s due to hostile conditions.", UnitType.getTypeName(actualUnitType))); // Gun emplacements use fixed tables instead of the force generator system } else if (actualUnitType == UnitType.GUN_EMPLACEMENT) { @@ -617,71 +591,43 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac // All other unit types use the force generator system to randomly select units } else { - // Determine unit types for each unit of the formation. Normally this is all one // type, but SPECIAL_UNIT_TYPE_ATB_MIX may generate all Meks, all vehicles, or // a Mek/vehicle mixed formation. - List unitTypes = generateUnitTypes(actualUnitType, lanceSize, quality, factionCode, - allowsTanks, campaign); + List unitTypes = generateUnitTypes(actualUnitType, lanceSize, quality, factionCode, allowsTanks, campaign); - // Formations composed entirely of Meks, aerospace fighters (but not - // conventional), + // Formations composed entirely of Meks, aerospace fighters (but not conventional), // and ground vehicles use weight categories as do SPECIAL_UNIT_TYPE_ATB_MIX. - // Formations of other types, plus artillery formations, do not use weight - // classes. - if ((actualUnitType == ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_MIX || - IUnitGenerator.unitTypeSupportsWeightClass(actualUnitType)) && - !forceTemplate.getUseArtillery()) { + // Formations of other types, plus artillery formations do not use weight classes. + if ((actualUnitType == SPECIAL_UNIT_TYPE_ATB_MIX || IUnitGenerator.unitTypeSupportsWeightClass(actualUnitType)) && !forceTemplate.getUseArtillery()) { // Generate a specific weight class for each unit based on the formation weight // class and lower/upper bounds - final String unitWeights = generateUnitWeights(unitTypes, factionCode, - AtBConfiguration.decodeWeightStr(currentLanceWeightString, 0), - forceTemplate.getMaxWeightClass(), - forceTemplate.getMinWeightClass(), - requiredRoles, - campaign); + final String unitWeights = generateUnitWeights(unitTypes, factionCode, AtBConfiguration.decodeWeightStr(currentLanceWeightString, 0), forceTemplate.getMaxWeightClass(), forceTemplate.getMinWeightClass(), requiredRoles, campaign); if (unitWeights != null) { - generatedLance = generateLance(factionCode, - skill, - quality, - unitTypes, - unitWeights, - requiredRoles, - campaign); + generatedLance = generateLance(factionCode, skill, quality, unitTypes, unitWeights, requiredRoles, campaign); } else { generatedLance = new ArrayList<>(); } } else { - generatedLance = generateLance(factionCode, - skill, - quality, - unitTypes, - requiredRoles, - campaign); + generatedLance = generateLance(factionCode, skill, quality, unitTypes, requiredRoles, campaign); // If extreme temperatures are present and XCT infantry is not being generated, // swap out standard armor for snowsuits or heat suits as appropriate if (actualUnitType == UnitType.INFANTRY) { for (Entity curPlatoon : generatedLance) { - changeInfantryKit((Infantry) curPlatoon, - isLowPressure, - isTainted, - scenario.getTemperature()); + changeInfantryKit((Infantry) curPlatoon, isLowPressure, isTainted, scenario.getTemperature()); } } } } // If something went wrong with unit generation, stop generating formations and - // work - // with what is already generated + // work with what is already generated if (generatedLance.isEmpty()) { stopGenerating = true; - logger.warn( - String.format("Unable to generate units from RAT: %s, type %d, max weight %d", - factionCode, forceTemplate.getAllowedUnitType(), weightClass)); + logger.warn(String.format("Unable to generate units from RAT: %s, type %d, max weight %d", factionCode, forceTemplate.getAllowedUnitType(), weightClass)); continue; } @@ -717,15 +663,7 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac ArrayList arrayGeneratedLance = new ArrayList<>(generatedLance); // bin fill ratio will be adjusted by the load out generator based on piracy and // quality - ReconfigurationParameters rp = TeamLoadOutGenerator.generateParameters( - cGame, - cGame.getOptions(), - arrayGeneratedLance, - factionCode, - new ArrayList<>(), - new ArrayList<>(), - ownerBaseQuality, - ((isPirate) ? TeamLoadOutGenerator.UNSET_FILL_RATIO : 1.0f)); + ReconfigurationParameters rp = TeamLoadOutGenerator.generateParameters(cGame, cGame.getOptions(), arrayGeneratedLance, factionCode, new ArrayList<>(), new ArrayList<>(), ownerBaseQuality, ((isPirate) ? TeamLoadOutGenerator.UNSET_FILL_RATIO : 1.0f)); rp.isPirate = isPirate; rp.groundMap = onGround; rp.spaceEnvironment = (mapLocation == MapLocation.Space); @@ -733,11 +671,7 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac tlg.reconfigureEntities(arrayGeneratedLance, factionCode, mt, rp); } else { // Load the fighters with bombs - TeamLoadOutGenerator.populateAeroBombs(generatedLance, - campaign.getGameYear(), - onGround, - ownerBaseQuality, - isPirate); + TeamLoadOutGenerator.populateAeroBombs(generatedLance, campaign.getGameYear(), onGround, ownerBaseQuality, isPirate); } } @@ -748,33 +682,32 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac setStartingAltitude(generatedLance, forceTemplate.getStartingAltitude()); correctNonAeroFlyerBehavior(generatedLance, scenario.getBoardType()); - // If force contributes to map size, increment the generated count of formations - // added + // If force contributes to map size, increment the generated count of formations added if (forceTemplate.getContributesToMapSize()) { generatedLanceCount++; } - // Check for mekanized battle armor added to Clan star formations (must be - // exactly - // 5 OmniMeks, no more, no less) - generatedLance.addAll(generateBAForNova(scenario, generatedLance, factionCode, skill, quality, campaign)); + // Check for mekanized battle armor added to Clan star formations (must be exactly 5 + // OmniMeks, no more, no less) + generatedLance.addAll(generateBAForNova(scenario, generatedLance, factionCode, skill, quality, campaign, false)); - // Add the formation member BVs to the running total, and the entities to the - // tracking + // Add the formation member BVs to the running total, and the entities to the tracking // list for (Entity ent : generatedLance) { - forceBV += ent.calculateBattleValue(); + if (campaign.getCampaignOptions().isUseGenericBattleValue()) { + forceBV += ent.getGenericBattleValue(); + } else { + forceBV += ent.calculateBattleValue(); + } generatedEntities.add(ent); } // Terminate force generation if we've gone over the unit count or BV budget. - // For BV-scaled forces, check whether to stop generating after each formation - // is + // For BV-scaled forces, check whether to stop generating after each formation is // generated. if (forceTemplate.getGenerationMethod() == ForceGenerationMethod.BVScaled.ordinal()) { // Check random number vs. percentage of the BV budget already generated, with - // the - // percentage chosen based on unit rating + // the percentage chosen based on unit rating double currentPercentage = ((double) forceBV / forceBVBudget) * 100; stopGenerating = currentPercentage > targetPercentage; @@ -786,67 +719,232 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac currentLanceWeightString = currentLanceWeightString.substring(1); } - // If over budget for both BV and unit count, pull units until it works + // If over budget for BV or unit count, pull units until it works while (forceUnitBudget > 0 && generatedEntities.size() > forceUnitBudget) { - generatedEntities.remove(Compute.randomInt(generatedEntities.size())); + int targetUnit = Compute.randomInt(generatedEntities.size()); + generatedEntities.remove(targetUnit); } if (forceTemplate.getGenerationMethod() == ForceGenerationMethod.BVScaled.ordinal()) { - while (((forceBV / forceBVBudget * 100) > targetPercentage) && (generatedEntities.size() > 1)) { - int targetEntity = Compute.randomInt(generatedEntities.size()); - forceBV -= generatedEntities.get(targetEntity).calculateBattleValue(); - generatedEntities.remove(targetEntity); + String balancingType = ""; + if (campaign.getCampaignOptions().isUseGenericBattleValue()) { + balancingType = " Generic"; + } + logger.info(String.format("Generated a force with %s / %s %s BV", + forceBV, forceBVBudget, balancingType)); + + int adjustedBvBudget = (int) (forceBVBudget * 1.25); + + while ((forceBV > adjustedBvBudget) && (generatedEntities.size() > 1)) { + int targetUnit = Compute.randomInt(generatedEntities.size()); + + int battleValue; + if (campaign.getCampaignOptions().isUseGenericBattleValue()) { + battleValue = generatedEntities.get(targetUnit).getGenericBattleValue(); + } else { + battleValue = generatedEntities.get(targetUnit).calculateBattleValue(); + } + + forceBV -= battleValue; + + logger.info(String.format("Culled %s (%s %s BV)", + generatedEntities.get(targetUnit).getDisplayName(), battleValue, balancingType)); + + generatedEntities.remove(targetUnit); } + + logger.info(String.format("Final force %s / %s %s BV", + forceBV, adjustedBvBudget, balancingType)); } // Units with infantry bays get conventional infantry or battle armor added - List transportedEntities = fillTransports(scenario, - generatedEntities, - factionCode, - skill, - quality, - requiredRoles, - allowsConvInfantry, - campaign); + List transportedEntities = fillTransports(scenario, generatedEntities, factionCode, + skill, quality, requiredRoles, allowsConvInfantry, campaign); generatedEntities.addAll(transportedEntities); if (!transportedEntities.isEmpty()) { - // Transported units need to filter out battle armor before applying armor - // changes - for (Entity curPlatoon : transportedEntities.stream().filter(i -> i.getUnitType() == UnitType.INFANTRY) - .toList()) { - changeInfantryKit((Infantry) curPlatoon, - isLowPressure, - isTainted, - scenario.getTemperature()); + // Transported units need to filter out battle armor before applying armor changes + for (Entity curPlatoon : transportedEntities.stream().filter(i -> i.getUnitType() == UnitType.INFANTRY).toList()) { + changeInfantryKit((Infantry) curPlatoon, isLowPressure, isTainted, scenario.getTemperature()); } } + // Generate the force BotForce generatedForce = new BotForce(); generatedForce.setFixedEntityList(generatedEntities); setBotForceParameters(generatedForce, forceTemplate, forceAlignment, contract); scenario.addBotForce(generatedForce, forceTemplate, campaign); + if (contract.isBatchallAccepted()) { + // Simulate bidding away of forces + List bidAwayForces = new ArrayList<>(); + int supplementedForces = 0; + + if (faction.isClan() && campaign.getCampaignOptions().isUseGenericBattleValue()) { + // Add dialog + bidAwayForces = new ArrayList<>(); + + // Player force values + int playerBattleValue = calculateEffectiveBV(scenario, campaign, true); + int playerUnitValue = calculateEffectiveUnitCount(scenario, campaign, true); + + // Bot force values + int botBattleValue = 0; + for (Entity entity : generatedForce.getFullEntityList(campaign)) { + botBattleValue += entity.calculateBattleValue(); + } + + // First bid away units that exceed the player's estimated Battle Value + while ((botBattleValue > (playerBattleValue * getHonorRating(campaign, factionCode))) + && (generatedForce.getFullEntityList(campaign).size() > 1)) { + int targetUnit = Compute.randomInt(generatedForce.getFullEntityList(campaign).size()); + bidAwayForces.add(generatedForce.getFullEntityList(campaign).get(targetUnit).getShortNameRaw()); + botBattleValue -= generatedForce.getFullEntityList(campaign).get(targetUnit).calculateBattleValue(); + generatedForce.removeEntity(targetUnit); + } + + // There is no point in adding extra Battle Armor to non-ground scenarios + if (scenario.getBoardType() == T_GROUND) { + // We want to purposefully exclude off-board artillery, to stop them being + // assigned random units of Battle Armor. + // If we ever implement the ability to move those units on-board, or for players + // to intercept off-board units, we'll probably want to remove this exclusion, + // so they can have some bodyguards. + if (!forceTemplate.getUseArtillery() && !forceTemplate.getDeployOffboard()) { + // Similarly, there is no value in adding random Battle Armor to aircraft forces + if (forceTemplate.getAllowedUnitType() != SPECIAL_UNIT_TYPE_ATB_MIX) { + // Next, if the size of the forces results in a Player:Bot unit count ratio of >= 2:1, + // add additional units of Battle Armor to compensate. + int sizeDisparity = playerUnitValue - generatedForce.getFullEntityList(campaign).size(); + sizeDisparity = (int) round(sizeDisparity * 0.5); + + List allRemainingUnits = new ArrayList<>(generatedForce.getFullEntityList(campaign)); + Collections.shuffle(allRemainingUnits); + + // First, attempt to add Mechanized Battle Armor + Iterator entityIterator = allRemainingUnits.iterator(); + while (entityIterator.hasNext() && sizeDisparity > 0) { + Entity entity = entityIterator.next(); + if (!entity.isOmni()) { + continue; + } + + List generatedBA = generateBAForNova(scenario, List.of(entity), + factionCode, skill, quality, campaign, true); + + if (!generatedBA.isEmpty()) { + for (Entity battleArmor : generatedBA) { + generatedForce.addEntity(battleArmor); + } + supplementedForces += generatedBA.size(); + sizeDisparity -= generatedBA.size(); + } + } + + // If there is still a disproportionate size disparity, add loose Battle Armor + for (int i = 0; i < sizeDisparity; i++) { + Entity newEntity = getEntity(factionCode, skill, quality, + UnitType.BATTLE_ARMOR, UNIT_WEIGHT_UNSPECIFIED, campaign); + if (newEntity != null) { + generatedForce.addEntity(newEntity); + supplementedForces++; + } + } + } + } + } + } + + // Report the bidding results (if any) to the player + if (generatedForce.getTeam() != 1) { + if (!bidAwayForces.isEmpty() || supplementedForces > 0) { + reportResultsOfBidding(scenario, campaign, bidAwayForces, generatedForce, + supplementedForces); + } + } + } + return generatedLanceCount; } /** - * Retrieves the unit rating from the given campaign. + * Calculates the honor rating for a given Clan. * - * @param campaign the campaign from which the unit rating is to be retrieved - * @return the unit rating value as an integer - */ - private static int getUnitRating(Campaign campaign) { - final CampaignOptions campaignOptions = campaign.getCampaignOptions(); - final UnitRatingMethod unitRatingMethod = campaignOptions.getUnitRatingMethod(); + * @param campaign the ongoing campaign + * @param factionCode the faction code for which to calculate honor rating + * @return the honor rating as a double value + */ + private static double getHonorRating(Campaign campaign, String factionCode) { + final double STRICT = 1.25; + final double OPPORTUNISTIC = 1.5; + final double LIBERAL = 1.75; + + boolean isPostInvasion = campaign.getLocalDate().getYear() > 3061; + + // This is based on the table found on page 274 of Total Warfare + // Any Clan not mentioned on that table is assumed to be Strict → Opportunistic + return switch (factionCode) { + case "CCC", "CHH", "CIH", "CNC", "CSR" -> OPPORTUNISTIC; + case "CCO", "CGS", "CSV" -> STRICT; + case "CGB", "CWIE" -> isPostInvasion ? STRICT : LIBERAL; + case "CDS" -> LIBERAL; + case "CW" -> isPostInvasion ? LIBERAL : OPPORTUNISTIC; + default -> isPostInvasion ? STRICT : OPPORTUNISTIC; + }; + } + + /** + * Reports the results of Clan bidding for a scenario. + * + * @param scenario the scenario for which bidding was done + * @param campaign the campaign in which the bidding took place + * @param bidAwayForces the list of forces bid away by the generated force + * @param generatedForce the force that generated the bid + * @param supplementedForces the number of additional Battle Armor units supplemented to the force + */ + private static void reportResultsOfBidding(AtBDynamicScenario scenario, Campaign campaign, + List bidAwayForces, BotForce generatedForce, + int supplementedForces) { + boolean useVerboseBidding = campaign.getCampaignOptions().isUseVerboseBidding(); + StringBuilder report = new StringBuilder(); + + if (useVerboseBidding) { + for (String unitName : bidAwayForces) { + if (report.isEmpty()) { + report.append(String.format(resources.getString("bidAwayForcesVerbose.text"), + generatedForce.getName(), scenario.getName(), scenario.getContract(campaign), + unitName)); + } else { + report.append(unitName).append("
"); + } + } + } else { + report.append(String.format(resources.getString("bidAwayForces.text"), + generatedForce.getName(), scenario.getName(), scenario.getContract(campaign), + bidAwayForces.size())); + } - int unitRating = IUnitRating.DRAGOON_C; - if (unitRatingMethod.isFMMR()) { - unitRating = campaign.getUnitRating().getUnitRatingAsInteger(); - } else if (unitRatingMethod.isCampaignOperations()) { - unitRating = campaign.getReputation().getAtbModifier(); + if (supplementedForces > 0) { + if (report.isEmpty()) { + report.append(String.format(resources.getString("addedBattleArmorNewReport.text"), + generatedForce.getName(), scenario.getName(), scenario.getContract(campaign), + supplementedForces)); + } else { + report.append(String.format( + resources.getString("addedBattleArmorContinueReport.text"), + supplementedForces)); + } } - return unitRating; + + if (!report.isEmpty()) { + if (Compute.randomInt(8) == 0) { + report.append(resources.getString("batchallConcludedVersion2.text")); + } else { + report.append(resources.getString("batchallConcludedVersion1.text")); + } + } + + campaign.addReport(report.toString()); } /** @@ -1086,7 +1184,7 @@ public static void setTerrain(AtBDynamicScenario scenario) { // if we are allowing all terrain types, then pick one from the list // otherwise, pick one from the allowed ones if (scenario.getTemplate().mapParameters.getMapLocation() == MapLocation.AllGroundTerrain) { - scenario.setBoardType(AtBScenario.T_GROUND); + scenario.setBoardType(T_GROUND); StratconBiomeManifest biomeManifest = StratconBiomeManifest.getInstance(); int kelvinTemp = scenario.getTemperature() + StratconContractInitializer.ZERO_CELSIUS_IN_KELVIN; List allowedTerrain = biomeManifest.getTempMap(StratconBiomeManifest.TERRAN_BIOME) @@ -1865,8 +1963,7 @@ private static Entity generateTransportedBAUnit(UnitGeneratorParameters params, newParams.getMovementModes().addAll(IUnitGenerator.ALL_BATTLE_ARMOR_MODES); // Set the parameters to filter out types that are too heavy for the provided - // bay space, - // or those that cannot use mechanized BA travel + // bay space, or those that cannot use mechanized BA travel if (bayCapacity != IUnitGenerator.NO_WEIGHT_LIMIT) { newParams.setFilter(inf -> inf.getTons() <= bayCapacity); } else { @@ -1875,8 +1972,7 @@ private static Entity generateTransportedBAUnit(UnitGeneratorParameters params, MekSummary unitData = campaign.getUnitGenerator().generate(newParams); - // If generating for an internal bay fails, try again as mechanized if the flag - // is set + // If generating for an internal bay fails, try again as mechanized if the flag is set if (unitData == null) { if (bayCapacity != IUnitGenerator.NO_WEIGHT_LIMIT && retryAsMechanized) { newParams.setFilter(null); @@ -1893,12 +1989,21 @@ private static Entity generateTransportedBAUnit(UnitGeneratorParameters params, } /** - * Worker function that generates a battle armor unit to attach to a unit of - * clan meks + * Generates and associates Battle Armor units with a designated transport. + * + * @param scenario The current scenario. + * @param starUnits List of {@link Entity} objects representing the star units. + * @param factionCode The code of the faction. + * @param skill The skill level. + * @param quality The quality level. + * @param campaign The current campaign. + * @param forceBASpawn Flag to determine whether to bypass the Star size check and BA + * spawn dice roll + * @return List of {@link Entity} objects representing the transported Battle Armor. */ public static List generateBAForNova(AtBScenario scenario, List starUnits, - String factionCode, SkillLevel skill, int quality, - Campaign campaign) { + String factionCode, SkillLevel skill, int quality, Campaign campaign, + boolean forceBASpawn) { List transportedUnits = new ArrayList<>(); // determine if this should be a nova @@ -1908,25 +2013,26 @@ public static List generateBAForNova(AtBScenario scenario, List // non-clan forces and units that aren't stars don't become novas // TODO: allow for non-Clan integrated mechanized formations, like WOB choirs, // as well as stars that are short one or more omnis - if (!Factions.getInstance().getFaction(factionCode).isClan() && (starUnits.size() != 5)) { + if (!Factions.getInstance().getFaction(factionCode).isClan() + && ((starUnits.size() != 5) || forceBASpawn)) { return transportedUnits; } - // logic copied from AtBScenario.addStar() to randomly determine if the given - // unit is actually going to be a nova - // adjusted from 11/8 to 8/6 (distribution of novas in newest AtB doc is a lot - // higher) so that players actually encounter novas - // whatever CBS is still gets no novas, so there - int roll = Compute.d6(2); - int novaTarget = 8; - if (factionCode.equals("CHH") || factionCode.equals("CSL")) { - novaTarget = 6; - } else if (factionCode.equals("CBS")) { - novaTarget = 13; - } + if (!forceBASpawn) { + // logic copied from AtBScenario.addStar() to randomly determine if the given + // unit is actually going to be a nova adjusted from 11/8 to 8/6 so that players + // actually encounter novas. + int roll = Compute.d6(2); + int novaTarget = 8; + if (factionCode.equals("CHH") || factionCode.equals("CSL")) { + novaTarget = 6; + } else if (factionCode.equals("CBS")) { + novaTarget = 13; + } - if (roll < novaTarget) { - return transportedUnits; + if (roll < novaTarget) { + return transportedUnits; + } } Entity actualTransport = null; @@ -2043,9 +2149,6 @@ private static Entity getEntityByName(String name, String factionCode, SkillLeve final AbstractSkillGenerator skillGenerator = new StratConSkillGenerator(); skillGenerator.setLevel(skill); - if (faction.isClan()) { - skillGenerator.setType(SkillGeneratorType.CLAN); - } int[] skills = skillGenerator.generateRandomSkills(en); if (faction.isClan() && (Compute.d6(2) > (6 - skill.ordinal() + skills[0] + skills[1]))) { @@ -2216,7 +2319,7 @@ private static List generateUnitTypes(int unitTypeCode, // This special unit type code randomly selects between all Mek, all vehicle, or // mixed // Mek/vehicle formations - if (unitTypeCode == ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_MIX) { + if (unitTypeCode == SPECIAL_UNIT_TYPE_ATB_MIX) { Faction faction = Factions.getInstance().getFaction(factionCode); // If ground vehicles are permitted in general and by environmental conditions, @@ -2236,20 +2339,39 @@ private static List generateUnitTypes(int unitTypeCode, return generateClanUnitTypes(unitCount, forceQuality, factionCode, campaign); } - // Use the Mek/vehicle/mixed ratios from campaign options as weighted values for - // random unit type - int totalWeight = campaign.getCampaignOptions().getOpForLanceTypeMeks() + - campaign.getCampaignOptions().getOpForLanceTypeMixed() + - campaign.getCampaignOptions().getOpForLanceTypeVehicles(); + // Use the Mek/Vehicle/Mixed ratios from campaign options as weighted values for + // random unit types. + // Then modify based on faction. + int mekLanceWeight = campaign.getCampaignOptions().getOpForLanceTypeMeks(); + int mixedLanceWeight = campaign.getCampaignOptions().getOpForLanceTypeMixed(); + int vehicleLanceWeight = campaign.getCampaignOptions().getOpForLanceTypeVehicles(); + + if (faction.isClan()) { + if (Objects.equals(factionCode, "CHH")) { + mixedLanceWeight++; + vehicleLanceWeight++; + } else { + mekLanceWeight++; + mixedLanceWeight = Math.max(0, mixedLanceWeight - 1); + vehicleLanceWeight = Math.max(0, vehicleLanceWeight - 1); + } + } else if (faction.isMinorPower() || faction.isPirate()) { + mekLanceWeight = Math.max(0, mekLanceWeight - 1); + mixedLanceWeight++; + vehicleLanceWeight++; + } + + int totalWeight = mekLanceWeight + mixedLanceWeight + vehicleLanceWeight; + + // Roll for unit types if (totalWeight <= 0) { actualUnitType = UnitType.MEK; } else { int roll = Compute.randomInt(totalWeight); - if (roll < campaign.getCampaignOptions().getOpForLanceTypeVehicles()) { + if (roll < vehicleLanceWeight) { actualUnitType = UnitType.TANK; - // Mixed units randomly select between Mek or ground vehicle - } else if (roll < campaign.getCampaignOptions().getOpForLanceTypeVehicles() + - campaign.getCampaignOptions().getOpForLanceTypeMixed()) { + // Mixed units randomly select between Mek or ground vehicle + } else if (roll < vehicleLanceWeight + mixedLanceWeight) { for (int x = 0; x < unitCount; x++) { boolean addTank = Compute.randomInt(2) == 0; if (addTank) { @@ -2267,9 +2389,12 @@ private static List generateUnitTypes(int unitTypeCode, actualUnitType = UnitType.MEK; } } + + // Add unit types to the list of actual unity types for (int x = 0; x < unitCount; x++) { unitTypes.add(actualUnitType); } + return unitTypes; } @@ -2432,7 +2557,8 @@ private static List generateClanUnitTypes(int unitCount, * @param campaign The campaign in which the scenario resides. * @return Effective BV. */ - public static int calculateEffectiveBV(AtBDynamicScenario scenario, Campaign campaign) { + public static int calculateEffectiveBV(AtBDynamicScenario scenario, Campaign campaign, + boolean forceStandardBattleValue) { // for each deployed player and bot force that's marked as contributing to the // BV budget int bvBudget = 0; @@ -2442,8 +2568,7 @@ public static int calculateEffectiveBV(AtBDynamicScenario scenario, Campaign cam for (int forceID : scenario.getForceIDs()) { ScenarioForceTemplate forceTemplate = scenario.getPlayerForceTemplates().get(forceID); if (forceTemplate != null && forceTemplate.getContributesToBV()) { - int forceBVBudget = (int) (campaign.getForce(forceID).getTotalBV(campaign) * difficultyMultiplier); - bvBudget += forceBVBudget; + bvBudget += campaign.getForce(forceID).getTotalBV(campaign, forceStandardBattleValue); } } @@ -2451,13 +2576,21 @@ public static int calculateEffectiveBV(AtBDynamicScenario scenario, Campaign cam for (UUID unitID : scenario.getIndividualUnitIDs()) { ScenarioForceTemplate forceTemplate = scenario.getPlayerUnitTemplates().get(unitID); if ((forceTemplate != null) && forceTemplate.getContributesToBV()) { - int unitBVBudget = (int) (campaign.getUnit(unitID).getEntity().calculateBattleValue() - * difficultyMultiplier); - bvBudget += unitBVBudget; + if (campaign.getCampaignOptions().isUseGenericBattleValue() && !forceStandardBattleValue) { + bvBudget += campaign.getUnit(unitID).getEntity().getGenericBattleValue(); + } else { + bvBudget += campaign.getUnit(unitID).getEntity().calculateBattleValue(); + } } } - bvBudget += (int) Math.round(bvBudget * scenario.getEffectivePlayerBVMultiplier()); + double bvMultiplier = scenario.getEffectivePlayerBVMultiplier(); + + if (bvMultiplier > 0) { + bvBudget = (int) round(bvBudget * scenario.getEffectivePlayerBVMultiplier() * difficultyMultiplier); + } else { + bvBudget = (int) round(bvBudget * difficultyMultiplier); + } // allied bot forces that contribute to BV do not get multiplied by the // difficulty @@ -2474,32 +2607,34 @@ public static int calculateEffectiveBV(AtBDynamicScenario scenario, Campaign cam } /** - * Calculates from scratch the current effective player and allied unit count - * present in the given scenario. + * Calculates the current effective player and allied unit count present in the given scenario. * * @param scenario The scenario to process. * @param campaign The campaign in which the scenario resides. - * @return Effective BV. + * @param isClanBidding {@link Boolean} flag indicating if Clan bidding is enabled. + * @return The effective unit count for the scenario. */ - public static int calculateEffectiveUnitCount(AtBDynamicScenario scenario, Campaign campaign) { + public static int calculateEffectiveUnitCount(AtBDynamicScenario scenario, Campaign campaign, + boolean isClanBidding) { // for each deployed player and bot force that's marked as contributing to the - // BV budget + // unit count budget int unitCount = 0; double difficultyMultiplier = getDifficultyMultiplier(campaign); // deployed player forces: for (int forceID : scenario.getForceIDs()) { ScenarioForceTemplate forceTemplate = scenario.getPlayerForceTemplates().get(forceID); + Force force = campaign.getForce(forceID); + if (forceTemplate != null && forceTemplate.getContributesToUnitCount()) { - int forceUnitCount = campaign.getForce(forceID).getUnits().size(); - unitCount += forceUnitCount; + unitCount += force.getTotalUnitCount(campaign, isClanBidding); } } // deployed individual player units for (UUID unitID : scenario.getIndividualUnitIDs()) { ScenarioForceTemplate forceTemplate = scenario.getPlayerUnitTemplates().get(unitID); - if ((forceTemplate != null) && forceTemplate.getContributesToBV()) { + if ((forceTemplate != null) && forceTemplate.getContributesToUnitCount()) { unitCount++; } } @@ -2508,8 +2643,7 @@ public static int calculateEffectiveUnitCount(AtBDynamicScenario scenario, Campa unitCount = (int) Math.floor((double) unitCount * difficultyMultiplier); // allied bot forces that contribute to BV do not get multiplied by the - // difficulty - // even if the player is super good, the AI doesn't get any better + // difficulty even if the player is good, the AI doesn't get any better for (int index = 0; index < scenario.getNumBots(); index++) { BotForce botForce = scenario.getBotForce(index); ScenarioForceTemplate forceTemplate = scenario.getBotForceTemplates().get(botForce); @@ -2540,42 +2674,23 @@ private static double getDifficultyMultiplier(Campaign campaign) { } /** - * Helper function that calculates the "weight class" of the player force. - * Putting any kind of dropship or other unit that doesn't fit into the - * "light-medium-heavy-assault" pattern - * will probably cause it to return "ASSAULT". + * Generates a random force weight for a given scenario. * - * @param scenario - * @param campaign - * @return + * @return the randomly generated {@link EntityWeightClass} */ - private static int calculatePlayerForceWeightClass(AtBDynamicScenario scenario, Campaign campaign) { - double weight = 0.0; - int unitCount = 0; + public static int randomForceWeight() { + int roll = Compute.randomInt(89); - for (int forceID : scenario.getForceIDs()) { - weight += Lance.calculateTotalWeight(campaign, forceID); - unitCount += campaign.getForce(forceID).getUnits().size(); - } - - int normalizedWeight = (int) (weight / unitCount); - - if (normalizedWeight < 20) { - return EntityWeightClass.WEIGHT_ULTRA_LIGHT; - } - if (normalizedWeight < 40) { + // These values are based the random force weight table found on page 265 of Total Warfare + if (roll < 19) { // 19% return EntityWeightClass.WEIGHT_LIGHT; - } - if (normalizedWeight < 60) { + } else if (roll < 50) { // 31% return EntityWeightClass.WEIGHT_MEDIUM; - } - if (normalizedWeight < 80) { + } else if (roll < 75) { // 25% return EntityWeightClass.WEIGHT_HEAVY; - } - if (normalizedWeight < 100) { + } else { // 14% return EntityWeightClass.WEIGHT_ASSAULT; } - return EntityWeightClass.WEIGHT_SUPER_HEAVY; } /** diff --git a/MekHQ/src/mekhq/campaign/mission/atb/AtBScenarioModifierApplicator.java b/MekHQ/src/mekhq/campaign/mission/atb/AtBScenarioModifierApplicator.java index 67433744ea..a558b6d06c 100644 --- a/MekHQ/src/mekhq/campaign/mission/atb/AtBScenarioModifierApplicator.java +++ b/MekHQ/src/mekhq/campaign/mission/atb/AtBScenarioModifierApplicator.java @@ -18,30 +18,18 @@ */ package mekhq.campaign.mission.atb; -import java.util.UUID; - import megamek.client.generator.enums.SkillGeneratorType; import megamek.client.generator.skillGenerators.AbstractSkillGenerator; import megamek.client.generator.skillGenerators.TaharqaSkillGenerator; import megamek.codeUtilities.MathUtility; -import megamek.common.Board; -import megamek.common.Compute; -import megamek.common.Entity; -import megamek.common.EntityWeightClass; -import megamek.common.HitData; -import megamek.common.Mounted; -import megamek.common.ToHitData; +import megamek.common.*; import megamek.common.enums.SkillLevel; import megamek.common.options.OptionsConstants; import megamek.logging.MMLogger; import mekhq.campaign.Campaign; import mekhq.campaign.force.Force; -import mekhq.campaign.mission.AtBDynamicScenario; -import mekhq.campaign.mission.AtBDynamicScenarioFactory; -import mekhq.campaign.mission.BotForce; -import mekhq.campaign.mission.ScenarioForceTemplate; +import mekhq.campaign.mission.*; import mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment; -import mekhq.campaign.mission.ScenarioObjective; import mekhq.campaign.mission.atb.AtBScenarioModifier.EventTiming; import mekhq.campaign.personnel.SkillType; import mekhq.campaign.personnel.Skills; @@ -49,6 +37,11 @@ import mekhq.campaign.unit.Unit; import mekhq.campaign.universe.Factions; +import java.util.UUID; + +import static mekhq.campaign.mission.AtBDynamicScenarioFactory.generateForce; +import static mekhq.campaign.mission.AtBDynamicScenarioFactory.randomForceWeight; + /** * Class that handles the application of scenario modifier actions to * AtBDynamicScenarios @@ -71,18 +64,23 @@ public static void addForce(Campaign campaign, AtBDynamicScenario scenario, Scen } /** - * Adds the given force to the scenario after primary forces have been - * generated. + * Adds the given force to the scenario after primary forces have been generated. + * + * @param campaign the current campaign + * @param scenario the current scenario + * @param templateToApply the force template to apply to the scenario */ private static void postAddForce(Campaign campaign, AtBDynamicScenario scenario, ScenarioForceTemplate templateToApply) { - int effectiveBV = AtBDynamicScenarioFactory.calculateEffectiveBV(scenario, campaign); - int effectiveUnitCount = AtBDynamicScenarioFactory.calculateEffectiveUnitCount(scenario, campaign); + int effectiveBV = AtBDynamicScenarioFactory.calculateEffectiveBV(scenario, campaign, false); + int effectiveUnitCount = AtBDynamicScenarioFactory.calculateEffectiveUnitCount(scenario, campaign, false); int deploymentZone = AtBDynamicScenarioFactory.calculateDeploymentZone(templateToApply, scenario, templateToApply.getForceName()); - AtBDynamicScenarioFactory.generateForce(scenario, scenario.getContract(campaign), campaign, - effectiveBV, effectiveUnitCount, EntityWeightClass.WEIGHT_ASSAULT, templateToApply, true); + int weightClass = randomForceWeight(); + + generateForce(scenario, scenario.getContract(campaign), campaign, effectiveBV, + effectiveUnitCount, weightClass, templateToApply, true); // the most recently added bot force is the one we just generated BotForce generatedBotForce = scenario.getBotForce(scenario.getNumBots() - 1); diff --git a/MekHQ/src/mekhq/campaign/universe/fameAndInfamy/BatchallFactions.java b/MekHQ/src/mekhq/campaign/universe/fameAndInfamy/BatchallFactions.java new file mode 100644 index 0000000000..06c7659c29 --- /dev/null +++ b/MekHQ/src/mekhq/campaign/universe/fameAndInfamy/BatchallFactions.java @@ -0,0 +1,92 @@ +package mekhq.campaign.universe.fameAndInfamy; + +import megamek.codeUtilities.MathUtility; +import megamek.common.Compute; +import mekhq.MekHQ; +import mekhq.campaign.Campaign; + +import java.util.List; +import java.util.ResourceBundle; + +/** + * Provides utility methods for working with clan factions within the Fame and Infamy module: + * determining whether they engage in batchalling, retrieving greetings and version strings for + * factions based on various conditions, such as the faction code, infamy level, and current year. + *

+ * The class is stateless and all methods are static, so it doesn't need to be instantiated. + * Therefore, all of its methods can be called directly on the class. + */ +public class BatchallFactions { + private static final ResourceBundle resources = ResourceBundle.getBundle( + "mekhq.resources.FameAndInfamy", + MekHQ.getMHQOptions().getLocale()); + + /** + * Determines whether a given faction engages in batchalling. + * + * @param factionCode The faction code to check eligibility for. Must be a non-null {@link String}. + * @return {@code true} if the faction code engages in batchalling, {@code false} otherwise. + */ + public static boolean usesBatchalls(String factionCode) { + if (factionCode == null) { + return false; + } + + List factionCodes = List.of("CBS", "CB", "CCC", "CCO", "CDS", "CFM", "CGB", + "CGS", "CHH", "CIH", "CJF", "CMG", "CNC", "CSJ", "CSR", "CSA", "CSV", "CSL", "CWI", "CW", + "CWE", "CWIE", "CEI", "RD", "RA", "CP", "AML", "CLAN"); + + return factionCodes.contains(factionCode); + } + + /** + * Retrieves the greeting for faction based on infamy. + * + * @param campaign The campaign for which to retrieve the greeting. + * @param factionCode The faction code for which to retrieve the greeting. + * @return The greeting message as a {@link String}. + */ + public static String getGreeting(Campaign campaign, String factionCode) { + final int infamy = MathUtility.clamp(campaign.getFameAndInfamy().getFameLevelForFaction(factionCode), + 0, 5); + + // Faction special handlers + String version = ""; + switch (factionCode) { + case "CWE" -> factionCode = "CW"; // Wolf Empire uses the lines for Clan Wolf + case "CGB" -> { // The Ghost Bear Dominion uses the lines for the Rasalhague Dominion + if (campaign.getGameYear() < 3060) { + factionCode = "CGB"; + } else { + factionCode = "RD"; + } + } + // Alyina Mercantile League isn't big enough to warrant their own lines, so they use the + // generic fallback + case "AML" -> factionCode = "CLAN"; + case "CDS" -> { // This handles the switch from Clan Diamond Shark to Clan Sea Fox + if (campaign.getGameYear() < 3100) { + version = "Version 1"; + } else { + version = "Version 2"; + } + } + default -> {} + } + + // The rest of the method + String greeting; + int type; + + if (infamy == 5) { + greeting = resources.getString("greetingCLANLevel5Type0.text"); + } else { + type = Compute.randomInt(3); + String greetingReference = String.format(resources.getString("greetingFormatBatchall.text"), + factionCode, version, infamy, type); + greeting = resources.getString(greetingReference); + } + + return greeting + '"'; + } +} diff --git a/MekHQ/src/mekhq/campaign/universe/fameAndInfamy/FameAndInfamyController.java b/MekHQ/src/mekhq/campaign/universe/fameAndInfamy/FameAndInfamyController.java new file mode 100644 index 0000000000..0b6b5acb3c --- /dev/null +++ b/MekHQ/src/mekhq/campaign/universe/fameAndInfamy/FameAndInfamyController.java @@ -0,0 +1,216 @@ +package mekhq.campaign.universe.fameAndInfamy; + +import megamek.codeUtilities.MathUtility; +import megamek.logging.MMLogger; +import mekhq.MekHQ; +import mekhq.campaign.Campaign; +import mekhq.campaign.universe.Factions; +import mekhq.utilities.MHQXMLUtility; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.File; +import java.io.PrintWriter; +import java.util.*; + +import static java.lang.Math.round; + +public class FameAndInfamyController { + private Map trackingFactions; + + private static final ResourceBundle resources = ResourceBundle.getBundle( + "mekhq.resources.FameAndInfamy", + MekHQ.getMHQOptions().getLocale()); + + private final static MMLogger logger = MMLogger.create(FameAndInfamyController.class); + + /** + * Constructor for the {@link FameAndInfamyController} class. + * Initializes the {@code trackingFactions} map with the provided map of factions. + * If any factions are missing from the provided map, they will be added with a default fame + * value of 3.0 (or 0.0 if the provided faction uses Batchalls). + */ + public FameAndInfamyController() { + this.trackingFactions = new HashMap<>(); + for (String shortName : getAllFactionShortnames()) { + if (BatchallFactions.usesBatchalls(shortName)) { + this.trackingFactions.put(shortName, 0.0); + } else { + this.trackingFactions.put(shortName, 2.0); + } + } + } + + /** + * Retrieves the shortnames of all factions from the XML file. + * + * @return A list of faction shortnames. + */ + public List getAllFactionShortnames() { + List shortnames = new ArrayList<>(); + + try { + File inputFile = new File("data/universe/factions.xml"); + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); + Document doc = dBuilder.parse(inputFile); + doc.getDocumentElement().normalize(); + + NodeList nList = doc.getElementsByTagName("shortname"); + + for (int temp = 0; temp < nList.getLength(); temp++) { + Node nNode = nList.item(temp); + + if (nNode.getNodeType() == Node.ELEMENT_NODE) { + Element element = (Element) nNode; + shortnames.add(element.getTextContent()); + } + } + } catch (Exception e) { + logger.error(String.format("FameAndInfamyController failed to parse contents of 'shortname'" + + " in 'data/universe/factions.xml'. Last successfully parsed Faction shortname: %s", + shortnames.get(shortnames.size() - 1))); + return shortnames; + } + + return shortnames; + } + + /** + * Retrieves the precise fame value for a given faction. + * Normally we don't care what the exact value is, + * so {@code getFameLevelForFaction} should be used, instead. + * + * @param factionCode the code of the faction + * @return the fame value for the faction + */ + public double getFameForFaction(String factionCode) { + return trackingFactions.get(factionCode); + } + + /** + * Retrieves the fame level for a faction. This is determined by normally rounding raw fame to + * the nearest {@link Integer} + * + * @param factionCode The code of the faction. + * @return The fame level of the faction. + */ + public int getFameLevelForFaction(String factionCode) { + return (int) round(trackingFactions.get(factionCode)); + } + + /** + * Retrieves the name of the fame level for a faction. + * + * @param factionCode The code of the faction. + * @param isInfamy Specifies whether to retrieve the fame name for infamy or fame. + * @return The name of the fame level for the faction. + */ + public String getFameName(String factionCode, boolean isInfamy) { + final int level = getFameLevelForFaction(factionCode); + + if (isInfamy) { + return switch (level) { + case 0 -> "Reviled"; + case 1 -> "Disgraced"; + case 2 -> "Insignificant"; + case 3 -> "Venerated"; + case 4 -> "Exalted"; + case 5 -> "Legendary"; + default -> throw new IllegalStateException("Unexpected value in getFameName, infamy: " + + level); + }; + } else { + return switch (level) { + case 0 -> "Insignificant"; + case 1 -> "Obscure"; + case 2 -> "Noted"; + case 3 -> "Notorious"; + case 4 -> "Infamous"; + case 5 -> "Reviled"; + default -> throw new IllegalStateException("Unexpected value in getFameName, fame: " + + level); + }; + } + } + + /** + * Sets the fame value for a specific faction. + * + * @param factionCode The code representing the faction. + * @param fame The fame value to be set for the faction. + */ + public void setFameForFaction(String factionCode, double fame) { + fame = MathUtility.clamp(fame, 0.0, 5.0); + + trackingFactions.put(factionCode, fame); + } + + /** + * Updates the fame of a faction by a specified adjustment. + * + * @param factionCode The code representing the faction. + * @param campaign The current campaign. + * @param adjustment The adjustment to be made to the faction's fame. + */ + public void updateFameForFaction(Campaign campaign, String factionCode, double adjustment) { + int originalFame = getFameLevelForFaction(factionCode); + + double currentFame = trackingFactions.get(factionCode); + adjustment = currentFame + adjustment; + adjustment = MathUtility.clamp(adjustment, 0.0, 5.0); + + trackingFactions.put(factionCode, adjustment); + + int newFame = getFameLevelForFaction(factionCode); + + if (originalFame != newFame) { + campaign.addReport(String.format(resources.getString("fameChangeReportInfamy.text"), + newFame, Factions.getInstance().getFaction(factionCode).getFullName(campaign.getGameYear()))); + } + } + + /** + * Writes the fame and infamy data to an XML file using the provided {@link PrintWriter} and indent level. + * + * @param printWriter The {@link PrintWriter} used to write to the XML file. + * @param indent The indent level for formatting the XML file. + */ + public void writeToXml(PrintWriter printWriter, int indent) { + MHQXMLUtility.writeSimpleXMLOpenTag(printWriter, indent++, "fameAndInfamy"); + for (String trackedFaction : trackingFactions.keySet()) { + double factionFame = trackingFactions.get(trackedFaction); + boolean shouldWrite = factionFame != (BatchallFactions.usesBatchalls(trackedFaction) ? 0 : 2); + + if (shouldWrite) { + MHQXMLUtility.writeSimpleXMLTag(printWriter, indent, trackedFaction, getFameForFaction(trackedFaction)); + } + } + MHQXMLUtility.writeSimpleXMLCloseTag(printWriter, --indent, "fameAndInfamy"); + } + + /** + * Parses the XML {@link NodeList} and updates the fame values for factions in a {@link Campaign}. + * + * @param nodeList The XML {@link NodeList} containing the faction fame values. + * @param campaign The {@link Campaign} object to update with the parsed fame values. + */ + public static void parseFromXML(final NodeList nodeList, Campaign campaign) { + FameAndInfamyController fameAndInfamy = campaign.getFameAndInfamy(); + try { + for (int x = 0; x < nodeList.getLength(); x++) { + final Node workingNode = nodeList.item(x); + if (workingNode.getNodeType() == Node.ELEMENT_NODE) { + double value = Double.parseDouble(workingNode.getTextContent()); + fameAndInfamy.setFameForFaction(workingNode.getNodeName(), value); + } + } + } catch (Exception e) { + logger.error(e); + } + } +} diff --git a/MekHQ/src/mekhq/gui/panes/CampaignOptionsPane.java b/MekHQ/src/mekhq/gui/panes/CampaignOptionsPane.java index f9877dff18..4897a0eea2 100644 --- a/MekHQ/src/mekhq/gui/panes/CampaignOptionsPane.java +++ b/MekHQ/src/mekhq/gui/panes/CampaignOptionsPane.java @@ -18,43 +18,6 @@ */ package mekhq.gui.panes; -import static megamek.client.ui.WrapLayout.wordWrap; - -import java.awt.BorderLayout; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.GridLayout; -import java.awt.Insets; -import java.awt.event.ActionEvent; -import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; -import java.time.LocalDate; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Hashtable; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.ResourceBundle; -import java.util.Set; -import java.util.Vector; -import java.util.stream.IntStream; - -import javax.swing.*; -import javax.swing.GroupLayout.Alignment; -import javax.swing.JSpinner.DefaultEditor; -import javax.swing.JSpinner.NumberEditor; -import javax.swing.LayoutStyle.ComponentPlacement; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; -import javax.swing.table.DefaultTableCellRenderer; -import javax.swing.table.DefaultTableModel; -import javax.swing.table.JTableHeader; -import javax.swing.table.TableColumn; - import megamek.client.generator.RandomGenderGenerator; import megamek.client.generator.RandomNameGenerator; import megamek.client.ui.baseComponents.JDisableablePanel; @@ -112,6 +75,29 @@ import mekhq.module.PersonnelMarketServiceManager; import mekhq.module.api.PersonnelMarketMethod; +import javax.swing.*; +import javax.swing.GroupLayout.Alignment; +import javax.swing.JSpinner.DefaultEditor; +import javax.swing.JSpinner.NumberEditor; +import javax.swing.LayoutStyle.ComponentPlacement; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.DefaultTableModel; +import javax.swing.table.JTableHeader; +import javax.swing.table.TableColumn; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.time.LocalDate; +import java.util.List; +import java.util.*; +import java.util.Map.Entry; +import java.util.stream.IntStream; + +import static megamek.client.ui.WrapLayout.wordWrap; + /** * @author Justin 'Windchild' Bowen */ @@ -680,6 +666,8 @@ public class CampaignOptionsPane extends AbstractMHQTabbedPane { private JCheckBox chkGenerateChases; // scenarios + private JCheckBox chkUseGenericBattleValue; + private JCheckBox chkUseVerboseBidding; private JCheckBox chkDoubleVehicles; private JSpinner spnOpForLanceTypeMeks; private JSpinner spnOpForLanceTypeMixed; @@ -3226,6 +3214,28 @@ private JScrollPane createAgainstTheBotTab() { panSubAtBContract.add(chkGenerateChases, gridBagConstraints); int yTablePosition = 0; + chkUseGenericBattleValue = new JCheckBox(resources.getString("chkUseGenericBattleValue.text")); + chkUseGenericBattleValue.setToolTipText(wordWrap(resources.getString("chkUseGenericBattleValue.toolTipText"))); + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = yTablePosition++; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = GridBagConstraints.NONE; + gridBagConstraints.insets = new Insets(5, 5, 5, 5); + gridBagConstraints.anchor = GridBagConstraints.NORTHWEST; + panSubAtBScenario.add(chkUseGenericBattleValue, gridBagConstraints); + + chkUseVerboseBidding = new JCheckBox(resources.getString("chkUseVerboseBidding.text")); + chkUseVerboseBidding.setToolTipText(wordWrap(resources.getString("chkUseVerboseBidding.toolTipText"))); + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = yTablePosition++; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = GridBagConstraints.NONE; + gridBagConstraints.insets = new Insets(5, 5, 5, 5); + gridBagConstraints.anchor = GridBagConstraints.NORTHWEST; + panSubAtBScenario.add(chkUseVerboseBidding, gridBagConstraints); + chkDoubleVehicles = new JCheckBox(resources.getString("chkDoubleVehicles.text")); chkDoubleVehicles.setToolTipText(resources.getString("chkDoubleVehicles.toolTipText")); gridBagConstraints = new GridBagConstraints(); @@ -9046,6 +9056,8 @@ public void setOptions(@Nullable CampaignOptions options, btnIntensityUpdate.doClick(); chkGenerateChases.setSelected(options.isGenerateChases()); + chkUseGenericBattleValue.setSelected(options.isUseGenericBattleValue()); + chkUseVerboseBidding.setSelected(options.isUseVerboseBidding()); chkDoubleVehicles.setSelected(options.isDoubleVehicles()); spnOpForLanceTypeMeks.setValue(options.getOpForLanceTypeMeks()); spnOpForLanceTypeMixed.setValue(options.getOpForLanceTypeMixed()); @@ -9634,6 +9646,8 @@ public void updateOptions() { options.setUseVehicles(chkUseVehicles.isSelected()); options.setClanVehicles(chkClanVehicles.isSelected()); options.setAutoConfigMunitions(chkAutoConfigMunitions.isSelected()); + options.setUseGenericBattleValue(chkUseGenericBattleValue.isSelected()); + options.setUseVerboseBidding(chkUseVerboseBidding.isSelected()); options.setDoubleVehicles(chkDoubleVehicles.isSelected()); options.setAdjustPlayerVehicles(chkAdjustPlayerVehicles.isSelected()); options.setOpForLanceTypeMeks((Integer) spnOpForLanceTypeMeks.getValue()); diff --git a/MekHQ/src/mekhq/gui/stratcon/ScenarioWizardLanceRenderer.java b/MekHQ/src/mekhq/gui/stratcon/ScenarioWizardLanceRenderer.java index 4b7190ac63..43bc129ae4 100644 --- a/MekHQ/src/mekhq/gui/stratcon/ScenarioWizardLanceRenderer.java +++ b/MekHQ/src/mekhq/gui/stratcon/ScenarioWizardLanceRenderer.java @@ -18,14 +18,13 @@ */ package mekhq.gui.stratcon; -import java.awt.*; - -import javax.swing.*; - import mekhq.campaign.Campaign; import mekhq.campaign.force.Force; import mekhq.campaign.force.Lance; +import javax.swing.*; +import java.awt.*; + /** * Handles rendering of individual lances in the StratCon scenario wizard. * @author NickAragua @@ -56,8 +55,8 @@ public Component getListCellRendererComponent(final JList list, if (lance != null) { roleString = lance.getRole().toString() + ", "; } - - setText(String.format("%s (%sBV: %d)", value.getName(), roleString, value.getTotalBV(campaign))); + + setText(String.format("%s (%sBV: %d)", value.getName(), roleString, value.getTotalBV(campaign, false))); return this; } diff --git a/MekHQ/userdata/data/universe/ranks.xml b/MekHQ/userdata/data/universe/ranks.xml index a84279a626..9222a5a93e 100644 --- a/MekHQ/userdata/data/universe/ranks.xml +++ b/MekHQ/userdata/data/universe/ranks.xml @@ -1,3 +1,3 @@ - +