diff --git a/megamek/mmconf/log4j2.xml b/megamek/mmconf/log4j2.xml index c59dd4e757b..c0623d4b5e1 100644 --- a/megamek/mmconf/log4j2.xml +++ b/megamek/mmconf/log4j2.xml @@ -1,5 +1,5 @@ - + %d{ABSOLUTE} %-5p [%logger{36}] {%t}%n%l - %m%n%ex%n %d{ABSOLUTE}\t%p\t[%logger{36}]\t{%t}\t%l\t%m\t%ex%n @@ -58,6 +58,15 @@ + + + + + + + + + @@ -65,7 +74,7 @@ - + diff --git a/megamek/mmconf/princessBehaviors.xml b/megamek/mmconf/princessBehaviors.xml index dffb84d99dc..afd7ae62c35 100644 --- a/megamek/mmconf/princessBehaviors.xml +++ b/megamek/mmconf/princessBehaviors.xml @@ -147,6 +147,7 @@ 10 4 8 + 2 WARNING @@ -162,6 +163,7 @@ 5 5 5 + 5 WARNING @@ -192,6 +194,7 @@ 10 8 2 + 1 WARNING diff --git a/megamek/resources/megamek/client/bot/messages.properties b/megamek/resources/megamek/client/bot/messages.properties index 106895b92b7..1775c9aa96d 100644 --- a/megamek/resources/megamek/client/bot/messages.properties +++ b/megamek/resources/megamek/client/bot/messages.properties @@ -108,3 +108,4 @@ Princess.command.artillery.order=Artillery order issued. Halt: orders the artill Princess.command.artillery.ammo=Special type of ammo to use. (Optional, default is None, it will use the best available ammo) Princess.command.artillery.noTargets=No targets were provided. Princess.command.artillery.description=Artillery - Orders off-board artillery to fire at the specified hexes. +Princess.command.reveal.description=Reveal - Forces all hidden units to reveal themselves. Useful for testing. diff --git a/megamek/resources/megamek/client/messages.properties b/megamek/resources/megamek/client/messages.properties index 23bc87a222b..ea4a1688dd9 100644 --- a/megamek/resources/megamek/client/messages.properties +++ b/megamek/resources/megamek/client/messages.properties @@ -4878,6 +4878,10 @@ BotConfigDialog.braverySliderMax=I fear nothing! BotConfigDialog.braverySliderMin=Run Away! BotConfigDialog.braverySliderTitle=Bravery {0} BotConfigDialog.braveryTooltip=Bravery: How worried about enemy damage am I? +BotConfigDialog.hiddenUnitRevealSliderMax=Ambush Freely +BotConfigDialog.hiddenUnitRevealSliderMin=Stay Hidden +BotConfigDialog.hiddenUnitRevealSliderTitle=Hidden Unit Reveal {0} +BotConfigDialog.hiddenUnitRevealTooltip=Hidden Unit Reveal: How willing am I to reveal hidden units to attack?
Lower values keep units hidden longer. Higher values reveal for smaller opportunities. BotConfigDialog.targetsLabel=Strategic Targets BotConfigDialog.addUnitTarget=+ Add Unit BotConfigDialog.addHexTarget=+ Add Hex diff --git a/megamek/src/megamek/client/bot/BotClient.java b/megamek/src/megamek/client/bot/BotClient.java index 3b3faefdead..bb1e99170aa 100644 --- a/megamek/src/megamek/client/bot/BotClient.java +++ b/megamek/src/megamek/client/bot/BotClient.java @@ -451,6 +451,10 @@ public void changePhase(GamePhase phase) { case DEPLOYMENT: initialize(); break; + case PREMOVEMENT: + evaluateHiddenUnitRepositioning(); + sendDone(true); + break; case MOVEMENT: /* * Do not uncomment this. It is so that bots stick around till end of game @@ -521,6 +525,14 @@ public void changePhase(GamePhase phase) { protected abstract void postMovementProcessing(); + /** + * Evaluates whether hidden units should reveal to reposition for better firing opportunities. Override in + * subclasses to implement intelligent hidden unit repositioning. Default implementation does nothing. + */ + protected void evaluateHiddenUnitRepositioning() { + // Default implementation - subclasses can override + } + private void runEndGame() { // Make a list of the player's living units. ArrayList living = game.getPlayerEntities(getLocalPlayer(), false); diff --git a/megamek/src/megamek/client/bot/princess/BehaviorSettings.java b/megamek/src/megamek/client/bot/princess/BehaviorSettings.java index 079aa6f3d0d..0220bd8d611 100644 --- a/megamek/src/megamek/client/bot/princess/BehaviorSettings.java +++ b/megamek/src/megamek/client/bot/princess/BehaviorSettings.java @@ -125,6 +125,22 @@ public class BehaviorSettings implements Serializable { 1.8, 2.0 }; + // Hidden unit reveal thresholds - higher index = more willing to reveal + // Values represent minimum (expectedDamage / selfPreservation) ratio to justify revealing + static final double[] HIDDEN_REVEAL_VALUES = { + 0.0, // 0: Never reveal + 0.5, // 1: Very conservative + 0.75, // 2: Conservative + 1.0, // 3: Cautious + 1.25, // 4: Moderate-conservative + 1.5, // 5: Moderate (default) + 2.0, // 6: Moderate-aggressive + 2.5, // 7: Aggressive + 3.0, // 8: Very aggressive + 4.0, // 9: Reckless + Double.MAX_VALUE // 10: Always reveal if any targets exist + }; + public static final int MAX_NUMBER_OF_ENEMIES_TO_CONSIDER_FACING = 12; public static final int MIN_NUMBER_OF_ENEMIES_TO_CONSIDER_FACING = 1; public static final int DEFAULT_NUMBER_OF_ENEMIES_TO_CONSIDER_FACING = 4; @@ -146,6 +162,7 @@ public class BehaviorSettings implements Serializable { private final Set priorityUnitTargets = new HashSet<>(); // What units do I especially want to blow up? private int herdMentalityIndex = 5; // How close do I want to stick to my teammates? private int braveryIndex = 5; // How quickly will I try to escape once damaged? + private int hiddenUnitRevealIndex = 5; // How willing to reveal hidden units to fire? private int antiCrowding = 0; // How much do I want to avoid crowding my teammates? private int favorHigherTMM = 0; // How much do I want to favor moving in my turn? private int numberOfEnemiesToConsiderFacing = DEFAULT_NUMBER_OF_ENEMIES_TO_CONSIDER_FACING; // How many enemies do I want to consider when calculating facing? @@ -174,6 +191,7 @@ public BehaviorSettings getCopy() throws PrincessException { copy.setDescription(getDescription()); copy.setFallShameIndex(getFallShameIndex()); copy.setBraveryIndex(getBraveryIndex()); + copy.setHiddenUnitRevealIndex(getHiddenUnitRevealIndex()); copy.setHerdMentalityIndex(getHerdMentalityIndex()); copy.setHyperAggressionIndex(getHyperAggressionIndex()); copy.setSelfPreservationIndex(getSelfPreservationIndex()); @@ -508,6 +526,57 @@ public void setBraveryIndex(final String index) throws PrincessException { } } + /** + * How willing am I to reveal hidden units to fire? + * + * @return Index of the current hidden unit reveal value. + */ + public int getHiddenUnitRevealIndex() { + return hiddenUnitRevealIndex; + } + + /** + * How willing am I to reveal hidden units to fire? + * + * @return Current hidden unit reveal threshold value. + */ + public double getHiddenUnitRevealValue() { + return getHiddenUnitRevealValue(hiddenUnitRevealIndex); + } + + /** + * How willing am I to reveal hidden units to fire? + * + * @param index The index [0-10] of the hidden unit reveal value desired. + * + * @return The hidden unit reveal threshold at the specified index. + */ + public double getHiddenUnitRevealValue(int index) { + return HIDDEN_REVEAL_VALUES[validateIndex(index)]; + } + + /** + * How willing am I to reveal hidden units to fire? + * + * @param index The index [0-10] of the hidden unit reveal value to be used. + */ + public void setHiddenUnitRevealIndex(final int index) { + this.hiddenUnitRevealIndex = validateIndex(index); + } + + /** + * How willing am I to reveal hidden units to fire? + * + * @param index The index ["0"-"10"] of the hidden unit reveal value to be used. + */ + public void setHiddenUnitRevealIndex(final String index) throws PrincessException { + try { + setHiddenUnitRevealIndex(Integer.parseInt(index)); + } catch (final NumberFormatException ex) { + throw new PrincessException(ex); + } + } + /** * @return The index of my current {@link #FALL_SHAME_VALUES}. */ @@ -898,6 +967,8 @@ boolean fromXml(final Element behavior) throws PrincessException { setHerdMentalityIndex(child.getTextContent()); } else if ("braveryIndex".equalsIgnoreCase(child.getNodeName())) { setBraveryIndex(child.getTextContent()); + } else if ("hiddenUnitRevealIndex".equalsIgnoreCase(child.getNodeName())) { + setHiddenUnitRevealIndex(child.getTextContent()); } else if ("antiCrowding".equalsIgnoreCase(child.getNodeName())) { setAntiCrowding(child.getTextContent()); } else if ("favorHigherTMM".equalsIgnoreCase(child.getNodeName())) { @@ -993,6 +1064,10 @@ Element toXml(final Document doc, braveryNode.setTextContent("" + getBraveryIndex()); behavior.appendChild(braveryNode); + final Element hiddenUnitRevealNode = doc.createElement("hiddenUnitRevealIndex"); + hiddenUnitRevealNode.setTextContent("" + getHiddenUnitRevealIndex()); + behavior.appendChild(hiddenUnitRevealNode); + final Element antiCrowdingNode = doc.createElement("antiCrowding"); antiCrowdingNode.setTextContent("" + getAntiCrowding()); behavior.appendChild(antiCrowdingNode); @@ -1069,6 +1144,8 @@ public String toLog() { out.append("\n\t Fall Shame: ").append(getFallShameIndex()).append(":") .append(getFallShameValue(getFallShameIndex())); out.append("\n\t Bravery: ").append(getBraveryIndex()).append(":").append(getBraveryValue(getBraveryIndex())); + out.append("\n\t Hidden Unit Reveal: ").append(getHiddenUnitRevealIndex()).append(":") + .append(getHiddenUnitRevealValue(getHiddenUnitRevealIndex())); out.append("\n\t AntiCrowding: ").append(getAntiCrowding()); out.append("\n\t FavorHigherTMM: ").append(getFavorHigherTMM()); out.append("\n\t NumberOfEnemiesToConsiderFacing: ").append(getNumberOfEnemiesToConsiderFacing()); @@ -1108,6 +1185,8 @@ public boolean equals(final Object o) { return false; } else if (braveryIndex != that.braveryIndex) { return false; + } else if (hiddenUnitRevealIndex != that.hiddenUnitRevealIndex) { + return false; } else if (fallShameIndex != that.fallShameIndex) { return false; } else if (forcedWithdrawal != that.forcedWithdrawal) { @@ -1167,6 +1246,7 @@ public int hashCode() { result = 31 * result + allowFacingTolerance; result = 31 * result + herdMentalityIndex; result = 31 * result + braveryIndex; + result = 31 * result + hiddenUnitRevealIndex; result = 31 * result + (exclusiveHerding ? 1 : 0); result = 31 * result + (iAmAPirate ? 1 : 0); result = 31 * result + (experimental ? 1 : 0); diff --git a/megamek/src/megamek/client/bot/princess/BehaviorSettingsFactory.java b/megamek/src/megamek/client/bot/princess/BehaviorSettingsFactory.java index 49f41b542e9..5d0a4d5f2fd 100644 --- a/megamek/src/megamek/client/bot/princess/BehaviorSettingsFactory.java +++ b/megamek/src/megamek/client/bot/princess/BehaviorSettingsFactory.java @@ -321,6 +321,7 @@ private BehaviorSettings buildBerserkBehavior() { berserkBehavior.setSelfPreservationIndex(2); berserkBehavior.setHerdMentalityIndex(5); berserkBehavior.setBraveryIndex(9); + berserkBehavior.setHiddenUnitRevealIndex(9); berserkBehavior.setAntiCrowding(0); berserkBehavior.setFavorHigherTMM(0); berserkBehavior.setNumberOfEnemiesToConsiderFacing(BehaviorSettings.DEFAULT_NUMBER_OF_ENEMIES_TO_CONSIDER_FACING); @@ -355,6 +356,7 @@ private BehaviorSettings buildCowardlyBehavior() { cowardlyBehavior.setSelfPreservationIndex(10); cowardlyBehavior.setHerdMentalityIndex(8); cowardlyBehavior.setBraveryIndex(2); + cowardlyBehavior.setHiddenUnitRevealIndex(1); cowardlyBehavior.setAntiCrowding(0); cowardlyBehavior.setFavorHigherTMM(0); cowardlyBehavior.setNumberOfEnemiesToConsiderFacing(BehaviorSettings.DEFAULT_NUMBER_OF_ENEMIES_TO_CONSIDER_FACING); @@ -390,6 +392,7 @@ private BehaviorSettings buildEscapeBehavior() { escapeBehavior.setSelfPreservationIndex(10); escapeBehavior.setHerdMentalityIndex(5); escapeBehavior.setBraveryIndex(2); + escapeBehavior.setHiddenUnitRevealIndex(2); escapeBehavior.setAntiCrowding(0); escapeBehavior.setFavorHigherTMM(0); escapeBehavior.setNumberOfEnemiesToConsiderFacing(BehaviorSettings.DEFAULT_NUMBER_OF_ENEMIES_TO_CONSIDER_FACING); @@ -425,6 +428,7 @@ private BehaviorSettings buildRuthlessBehavior() { ruthlessBehavior.setSelfPreservationIndex(10); ruthlessBehavior.setHerdMentalityIndex(1); ruthlessBehavior.setBraveryIndex(7); + ruthlessBehavior.setHiddenUnitRevealIndex(7); ruthlessBehavior.setAntiCrowding(10); ruthlessBehavior.setFavorHigherTMM(8); ruthlessBehavior.setNumberOfEnemiesToConsiderFacing(2); @@ -460,6 +464,7 @@ private BehaviorSettings buildPirateBehavior() { pirateBehavior.setSelfPreservationIndex(6); pirateBehavior.setHerdMentalityIndex(9); pirateBehavior.setBraveryIndex(10); + pirateBehavior.setHiddenUnitRevealIndex(8); pirateBehavior.setAntiCrowding(5); pirateBehavior.setFavorHigherTMM(5); pirateBehavior.setIAmAPirate(true); @@ -498,6 +503,7 @@ private BehaviorSettings buildConvoyBehavior() { convoyBehavior.setFallShameIndex(6); convoyBehavior.setHyperAggressionIndex(3); convoyBehavior.setBraveryIndex(2); + convoyBehavior.setHiddenUnitRevealIndex(0); convoyBehavior.setSelfPreservationIndex(10); convoyBehavior.setHerdMentalityIndex(5); convoyBehavior.setAntiCrowding(5); @@ -538,6 +544,7 @@ private BehaviorSettings buildDefaultBehavior() { defaultBehavior.setSelfPreservationIndex(5); defaultBehavior.setHerdMentalityIndex(5); defaultBehavior.setBraveryIndex(5); + defaultBehavior.setHiddenUnitRevealIndex(5); defaultBehavior.setAntiCrowding(0); defaultBehavior.setFavorHigherTMM(0); defaultBehavior.setNumberOfEnemiesToConsiderFacing(BehaviorSettings.DEFAULT_NUMBER_OF_ENEMIES_TO_CONSIDER_FACING); diff --git a/megamek/src/megamek/client/bot/princess/ChatCommands.java b/megamek/src/megamek/client/bot/princess/ChatCommands.java index df49b5ba537..85363946e6a 100644 --- a/megamek/src/megamek/client/bot/princess/ChatCommands.java +++ b/megamek/src/megamek/client/bot/princess/ChatCommands.java @@ -114,7 +114,11 @@ public enum ChatCommands { SET_WAYPOINT("sw", "set-waypoints", Messages.getString("Princess.command.setWaypoints.description"), - new SetWaypointsCommand()); + new SetWaypointsCommand()), + REVEAL("rv", + "reveal", + Messages.getString("Princess.command.reveal.description"), + new RevealCommand()); private final String abbreviation; private final String command; diff --git a/megamek/src/megamek/client/bot/princess/EnemyTracker.java b/megamek/src/megamek/client/bot/princess/EnemyTracker.java index 20949b1a603..bd009808ed0 100644 --- a/megamek/src/megamek/client/bot/princess/EnemyTracker.java +++ b/megamek/src/megamek/client/bot/princess/EnemyTracker.java @@ -42,13 +42,14 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import megamek.common.compute.Compute; +import megamek.common.ToHitData; +import megamek.common.actions.WeaponAttackAction; import megamek.common.board.Coords; -import megamek.common.units.Entity; +import megamek.common.compute.Compute; import megamek.common.game.Game; +import megamek.common.units.Entity; import megamek.common.units.Targetable; -import megamek.common.ToHitData; -import megamek.common.actions.WeaponAttackAction; +import megamek.logging.MMLogger; /** * Tracks enemy units and calculates threat scores based on their positions and capabilities. @@ -56,6 +57,8 @@ * @author Luana Coppio */ public class EnemyTracker { + private static final MMLogger LOGGER = MMLogger.create(EnemyTracker.class); + private final Map enemyProfiles = new HashMap<>(); private final Princess owner; @@ -165,9 +168,16 @@ public void updateThreatAssessment(Coords currentSwarmCenter) { && enemyProfiles.size() == visibleEnemies.size()) { return; } + + LOGGER.debug("Round {}: Updating threat assessment - {} visible enemies", + currentRound, visibleEnemies.size()); + lastRoundUpdate = getOwner().getGame().getCurrentRound(); + int previousProfileCount = enemyProfiles.size(); + // 1. Update known enemy positions/states visibleEnemies.forEach(enemy -> { + boolean isNewEnemy = !enemyProfiles.containsKey(enemy.getId()); EnemyProfile profile = enemyProfiles.computeIfAbsent(enemy.getId(), id -> new EnemyProfile(enemy)); @@ -183,18 +193,38 @@ public void updateThreatAssessment(Coords currentSwarmCenter) { averageDamagePotential, currentRound ); + + if (isNewEnemy) { + LOGGER.debug(" [NEW] Detected enemy: {} at {} (avg damage potential: {})", + enemy.getDisplayName(), enemy.getPosition(), String.format("%.1f", averageDamagePotential)); + } else { + LOGGER.debug(" [UPDATE] Tracking enemy: {} at {} (avg damage potential: {})", + enemy.getDisplayName(), enemy.getPosition(), String.format("%.1f", averageDamagePotential)); + } }); // remove all the dead entities and the entities that have not been seen for 3 turns - enemyProfiles.entrySet().removeIf(entry -> - (owner.getGame().getEntity(entry.getValue().id()) == null) - || (entry.getValue().getLastSeenTurn() < currentRound - 3) - ); + enemyProfiles.entrySet().removeIf(entry -> { + Entity entity = owner.getGame().getEntity(entry.getValue().id()); + boolean shouldRemove = (entity == null) + || (entry.getValue().getLastSeenTurn() < currentRound - 3); + if (shouldRemove && entity != null) { + LOGGER.debug(" [LOST] Lost track of enemy: {} (last seen round {})", + entity.getDisplayName(), entry.getValue().getLastSeenTurn()); + } + return shouldRemove; + }); // 3. Calculate threat scores enemyProfiles.values().forEach(profile -> profile.calculateThreatScore(currentSwarmCenter) ); + + int newEnemies = enemyProfiles.size() - previousProfileCount; + if (newEnemies > 0) { + LOGGER.debug("Round {}: Now tracking {} enemies ({} new this round)", + currentRound, enemyProfiles.size(), newEnemies); + } } private static class EnemyProfile implements Comparable { diff --git a/megamek/src/megamek/client/bot/princess/Princess.java b/megamek/src/megamek/client/bot/princess/Princess.java index fc0faaf294f..e58eabf323e 100644 --- a/megamek/src/megamek/client/bot/princess/Princess.java +++ b/megamek/src/megamek/client/bot/princess/Princess.java @@ -48,6 +48,7 @@ import megamek.client.bot.princess.PathRanker.PathRankerType; import megamek.client.bot.princess.UnitBehavior.BehaviorType; import megamek.client.bot.princess.coverage.Builder; +import megamek.client.bot.princess.geometry.CoordFacingCombo; import megamek.client.ui.SharedUtility; import megamek.client.ui.panels.phaseDisplay.TowLinkWarning; import megamek.codeUtilities.MathUtility; @@ -67,6 +68,7 @@ import megamek.common.actions.EntityAction; import megamek.common.actions.FindClubAction; import megamek.common.actions.SearchlightAttackAction; +import megamek.common.actions.TorsoTwistAction; import megamek.common.actions.WeaponAttackAction; import megamek.common.annotations.Nullable; import megamek.common.battleArmor.BattleArmor; @@ -80,6 +82,7 @@ import megamek.common.compute.Compute; import megamek.common.containers.PlayerIDAndList; import megamek.common.enums.AimingMode; +import megamek.common.enums.GamePhase; import megamek.common.enums.MoveStepType; import megamek.common.equipment.AmmoType; import megamek.common.equipment.EquipmentMode; @@ -203,6 +206,11 @@ public class Princess extends BotClient { private final ConcurrentHashMap damageMap = new ConcurrentHashMap<>(); private final ConcurrentHashMap teamTagTargetsMap = new ConcurrentHashMap<>(); private final ConcurrentHashMap, List> incomingGuidablesMap = new ConcurrentHashMap<>(); + /** + * Tracks expected damage history for hidden units to analyze firing opportunity trends. Maps entity ID to list of + * (round, expectedDamage) pairs for trend analysis. + */ + private final Map> hiddenUnitDamageHistory = new ConcurrentHashMap<>(); private final Set strategicBuildingTargets = new HashSet<>(); private boolean fallBack = false; private final ChatProcessor chatProcessor = new ChatProcessor(); @@ -927,6 +935,16 @@ protected Coords rankDeploymentCoords(Entity deployedUnit, List possible @Override protected void calculateFiringTurn() { + // Log visible enemies at start of firing phase + List enemies = getEnemyEntities(); + if (!enemies.isEmpty()) { + LOGGER.debug("Firing phase - {} visible enemies: {}", + enemies.size(), + enemies.stream().map(Entity::getDisplayName).collect(java.util.stream.Collectors.joining(", "))); + } else { + LOGGER.debug("Firing phase - no visible enemies detected"); + } + final Entity shooter; try { // get the first entity that can act this turn make sure weapons @@ -968,8 +986,16 @@ protected void calculateFiringTurn() { } if (shooter.isHidden()) { - skipFiring = true; - LOGGER.info("Hidden unit skips firing."); + skipFiring = !shouldRevealHiddenUnit(shooter); + if (skipFiring) { + LOGGER.info("Hidden unit {} remains concealed.", shooter.getDisplayName()); + } else { + // Reveal the unit before firing - per hidden unit rules, + // a unit must reveal itself to attack + shooter.setHidden(false); + sendUpdateEntity(shooter); + LOGGER.info("Hidden unit {} reveals to engage targets.", shooter.getDisplayName()); + } } // calculating a firing plan is somewhat expensive, so @@ -993,6 +1019,16 @@ protected void calculateFiringTurn() { shooter.getDisplayName(), plan.getDebugDescription(true)); + // Log twist/facing diagnostic info + LOGGER.debug("{} - Twist/Facing: planTwist={}, currentFacing={}, secondaryFacing={}, " + + "canChangeSecondaryFacing={}, alreadyTwisted={}", + shooter.getDisplayName(), + plan.getTwist(), + shooter.getFacing(), + shooter.getSecondaryFacing(), + shooter.canChangeSecondaryFacing(), + shooter.getAlreadyTwisted()); + // Consider making an aimed shot if the target is shut down or the attacker has // a targeting computer. Alternatively, consider using the called shots optional // rule to adjust the hit table to something more favorable. @@ -1099,6 +1135,18 @@ protected void calculateFiringTurn() { actions.add(spotAction); } + // Log actions being sent, especially any twist action + for (EntityAction action : actions) { + if (action instanceof TorsoTwistAction tta) { + LOGGER.debug("{} - Sending TorsoTwistAction: newFacing={}", + shooter.getDisplayName(), tta.getFacing()); + } + } + if (plan.getTwist() != 0 && actions.stream().noneMatch(a -> a instanceof TorsoTwistAction)) { + LOGGER.warn("{} - Plan has twist={} but NO TorsoTwistAction in actions!", + shooter.getDisplayName(), plan.getTwist()); + } + sendAttackData(shooter.getId(), actions); return; } else { @@ -3963,4 +4011,548 @@ private double probabilityOfMajorityWinOnReroll(List enemyRolls) { return (double) favorableOutcomes / totalOutcomes; } + + /** + * Determines whether a hidden unit should reveal itself to fire. Evaluates the potential firing opportunity against + * the reveal threshold from behavior settings. Higher thresholds require better opportunities. + * + * @param shooter The hidden unit considering whether to reveal + * + * @return true if the unit should reveal and fire, false to stay hidden + */ + private boolean shouldRevealHiddenUnit(Entity shooter) { + final double baseThreshold = getBehaviorSettings().getHiddenUnitRevealValue(); + + // Index 0 means never reveal + if (baseThreshold == 0.0) { + LOGGER.debug("Hidden unit {} has reveal threshold 0 - staying hidden.", + shooter.getDisplayName()); + return false; + } + + // Index 10 means always reveal if any target exists + if (baseThreshold == Double.MAX_VALUE) { + LOGGER.debug("Hidden unit {} has reveal threshold MAX - checking for any targets.", + shooter.getDisplayName()); + return hasAnyValidTarget(); + } + + // Calculate the best potential firing plan for evaluation (do this first for trend tracking) + final Map ammoConservation = calcAmmoConservation(shooter); + final FiringPlan plan = getFireControl(shooter).getBestFiringPlan( + shooter, getHonorUtil(), game, ammoConservation); + + double expectedDamage = (plan != null && !plan.isEmpty()) ? plan.getExpectedDamage() : 0; + + // Record expected damage for trend analysis (even if 0, to track when targets leave) + recordHiddenUnitDamage(shooter.getId(), expectedDamage); + + if (plan == null || plan.isEmpty() || expectedDamage <= 0) { + LOGGER.debug("Hidden unit {} has no valid firing plan - staying hidden.", + shooter.getDisplayName()); + return false; + } + + // Apply unit type multiplier - infantry/BA are better suited for spotting + double unitTypeMultiplier = calculateUnitTypeThresholdMultiplier(shooter); + + // Apply spotter value multiplier - if friendlies have indirect fire, we're more valuable as spotter + double spotterValueMultiplier = calculateSpotterValueMultiplier(shooter); + + // Apply damage trend multiplier - accounts for whether opportunity is improving or declining + double trendMultiplier = calculateDamageTrendMultiplier(shooter.getId(), expectedDamage); + + // Calculate effective threshold with all multipliers applied + double effectiveThreshold = baseThreshold * unitTypeMultiplier * spotterValueMultiplier * trendMultiplier; + + // Calculate reveal decision + // Decision formula: reveal if (expected damage / self-preservation factor) >= threshold + // Higher self-preservation = need more damage to justify reveal + // Higher threshold = more conservative about revealing + double selfPreservation = getBehaviorSettings().getSelfPreservationValue(); + double revealScore = expectedDamage / selfPreservation; + + LOGGER.debug("Hidden unit {} reveal evaluation: expectedDamage={}, selfPreservation={}, " + + "revealScore={}, baseThreshold={}, unitTypeMultiplier={}, spotterValueMultiplier={}, " + + "trendMultiplier={}, effectiveThreshold={}", + shooter.getDisplayName(), String.format("%.2f", expectedDamage), selfPreservation, + String.format("%.4f", revealScore), baseThreshold, unitTypeMultiplier, spotterValueMultiplier, + trendMultiplier, String.format("%.4f", effectiveThreshold)); + + // Log twist/facing info for debugging arc issues + LOGGER.debug("Hidden unit {} firing plan requires twist={}, shooter facing={}, secondaryFacing={}, " + + "canChangeSecondaryFacing={}", + shooter.getDisplayName(), plan.getTwist(), shooter.getFacing(), shooter.getSecondaryFacing(), + shooter.canChangeSecondaryFacing()); + + return revealScore >= effectiveThreshold; + } + + /** + * Checks if there are any valid enemy targets on the battlefield. Used for the "always reveal" + * setting (index 10). + * + * @return true if there are valid targets, false otherwise + */ + private boolean hasAnyValidTarget() { + List targets = FireControl.getAllTargetableEnemyEntities( + getLocalPlayer(), getGame(), getFireControlState()); + return !targets.isEmpty(); + } + + /** + * Calculates the threshold multiplier based on unit type. Infantry and Battle Armor have higher thresholds (harder + * to reveal) because they have limited range and are better suited for spotting roles. + * + * @param entity The entity to check + * + * @return The threshold multiplier (1.0 = no change, higher = harder to reveal) + */ + private double calculateUnitTypeThresholdMultiplier(Entity entity) { + // Conventional infantry (not Battle Armor, not MekWarrior) - much higher threshold + if (entity.hasETypeFlag(Entity.ETYPE_INFANTRY) + && !entity.hasETypeFlag(Entity.ETYPE_BATTLEARMOR) + && !entity.hasETypeFlag(Entity.ETYPE_MEKWARRIOR)) { + return 3.0; + } + + // Battle Armor - moderately higher threshold + if (entity.hasETypeFlag(Entity.ETYPE_BATTLEARMOR)) { + return 1.5; + } + + // All other unit types - no change + return 1.0; + } + + /** + * Calculates the spotter value multiplier for hidden unit reveal decision. If the hidden unit can see targets AND + * friendly units have indirect fire capability, the hidden unit is more valuable as a spotter, so we increase the + * reveal threshold. + * + * @param shooter The hidden unit considering revealing + * + * @return The spotter value multiplier (1.0 = no spotter value, 1.5 = valuable as spotter) + */ + private double calculateSpotterValueMultiplier(Entity shooter) { + // First check: can this unit spot? (not sprinting, not evading, etc.) + if (!shooter.canSpot()) { + return 1.0; + } + + // Second check: are there any targets this unit can see? + List visibleTargets = getVisibleTargetsForSpotter(shooter); + if (visibleTargets.isEmpty()) { + return 1.0; + } + + // Third check: do any friendly units have indirect fire capability? + boolean friendlyHasIndirectFire = hasFriendlyIndirectFireCapability(shooter); + if (!friendlyHasIndirectFire) { + return 1.0; + } + + LOGGER.debug("Hidden unit {} has spotter value: can see {} targets and friendly units have indirect fire", + shooter.getDisplayName(), visibleTargets.size()); + + // This unit is valuable as a spotter - increase reveal threshold + return 1.5; + } + + /** + * Gets targets visible to the potential spotter (for spotter value calculation). Only includes ground targets in + * LOS. + * + * @param spotter The entity checking for visible targets + * + * @return List of visible targets + */ + private List getVisibleTargetsForSpotter(Entity spotter) { + List visibleTargets = new ArrayList<>(); + List allTargets = FireControl.getAllTargetableEnemyEntities( + getLocalPlayer(), getGame(), getFireControlState()); + + for (Targetable target : allTargets) { + // Skip airborne targets (can't spot them for LRM indirect fire) + if (target.isAirborne()) { + continue; + } + + // Check LOS + LosEffects effects = LosEffects.calculateLOS(game, spotter, target); + if (effects.canSee()) { + visibleTargets.add(target); + } + } + + return visibleTargets; + } + + /** + * Checks if any friendly unit (excluding the specified shooter) has indirect fire capability. + * + * @param excludeEntity Entity to exclude from the check (typically the hidden unit itself) + * + * @return true if at least one friendly unit can use indirect fire + */ + private boolean hasFriendlyIndirectFireCapability(Entity excludeEntity) { + FireControl fireControl = getFireControl(excludeEntity); + + for (Entity friendly : getEntitiesOwned()) { + if (friendly.equals(excludeEntity)) { + continue; + } + + // Skip destroyed or off-board units + if (friendly.isDestroyed() || friendly.isOffBoard()) { + continue; + } + + // Use existing infrastructure to check for indirect fire capability + if (fireControl.entityCanIndirectFireMissile(getFireControlState(), friendly)) { + return true; + } + } + + return false; + } + + /** + * Records the expected damage for a hidden unit this round for trend analysis. + * + * @param entityId The entity ID of the hidden unit + * @param expectedDamage The expected damage calculated this round + */ + private void recordHiddenUnitDamage(int entityId, double expectedDamage) { + int currentRound = game.getCurrentRound(); + hiddenUnitDamageHistory.computeIfAbsent(entityId, k -> new ArrayList<>()) + .add(new double[] { currentRound, expectedDamage }); + + // Keep only last 7 rounds of history + List history = hiddenUnitDamageHistory.get(entityId); + while (history.size() > 7) { + history.remove(0); + } + } + + /** + * Analyzes the damage trend for a hidden unit and returns a multiplier for the reveal threshold. A declining trend + * (past peak) makes it easier to reveal (lower multiplier). An increasing trend makes it harder to reveal (higher + * multiplier) to wait for better opportunity. + * + * @param entityId The entity ID of the hidden unit + * @param currentExpectedDamage The expected damage calculated this round + * + * @return Threshold multiplier: less than 1.0 = easier to reveal, greater than 1.0 = harder to reveal + */ + private double calculateDamageTrendMultiplier(int entityId, double currentExpectedDamage) { + List history = hiddenUnitDamageHistory.get(entityId); + + // Need at least 3 data points for trend analysis + if (history == null || history.size() < 3) { + return 1.0; + } + + // Find the peak damage in history + double peakDamage = 0; + int peakIndex = 0; + for (int i = 0; i < history.size(); i++) { + if (history.get(i)[1] > peakDamage) { + peakDamage = history.get(i)[1]; + peakIndex = i; + } + } + + // Calculate recent trend (last 3 values) + int size = history.size(); + double oldest = history.get(size - 3)[1]; + double middle = history.get(size - 2)[1]; + double newest = history.get(size - 1)[1]; + + // Determine trend direction + boolean increasingTrend = newest > middle && middle > oldest; + boolean decreasingTrend = newest < middle && middle < oldest; + boolean atOrNearPeak = peakIndex >= size - 2; // Peak was in last 2 rounds + + // Calculate how close current damage is to peak (as percentage) + double peakRatio = peakDamage > 0 ? currentExpectedDamage / peakDamage : 0; + + LOGGER.debug("Hidden unit damage trend: history size={}, peak={}, current={}, peakRatio={}, " + + "increasing={}, decreasing={}, atPeak={}", + size, String.format("%.1f", peakDamage), String.format("%.1f", currentExpectedDamage), + String.format("%.2f", peakRatio), increasingTrend, decreasingTrend, atOrNearPeak); + + // Apply multipliers based on trend + if (atOrNearPeak && peakRatio >= 0.9) { + // At or near peak - good time to reveal, make it easier + return 0.75; + } else if (decreasingTrend && peakRatio < 0.7) { + // Past peak and declining significantly - missed best opportunity but still consider revealing + // Make it slightly easier since waiting longer won't help + return 0.85; + } else if (increasingTrend && peakRatio < 0.6) { + // Still climbing toward peak - wait for better opportunity + return 1.25; + } else if (increasingTrend && peakRatio >= 0.6) { + // Climbing and getting close to good damage - slight wait + return 1.1; + } + + // Flat or unclear trend - no adjustment + return 1.0; + } + + /** + * Cleans up damage history for units that are no longer hidden or no longer exist. Should be called periodically to + * prevent memory leaks. + */ + void cleanupHiddenUnitDamageHistory() { + hiddenUnitDamageHistory.entrySet().removeIf(entry -> { + Entity entity = game.getEntity(entry.getKey()); + return entity == null || !entity.isHidden(); + }); + } + + // ==================== Hidden Unit Repositioning ==================== + + /** + * Threshold multiplier for repositioning decision. Hidden unit will reveal to move if the expected damage from the + * best reposition location is at least this many times better than current position damage. + */ + private static final double REPOSITION_DAMAGE_THRESHOLD = 2.0; + + /** + * Evaluates whether hidden units should reveal and reposition for better firing opportunities. Called during + * PREMOVEMENT phase. If a hidden unit would have significantly better damage output from a different position, mark + * it to reveal at the start of movement phase. + */ + @Override + protected void evaluateHiddenUnitRepositioning() { + // Only evaluate if hidden units game option is enabled + if (!game.getOptions().booleanOption(OptionsConstants.ADVANCED_HIDDEN_UNITS)) { + return; + } + + List enemies = getEnemyEntities(); + if (enemies.isEmpty()) { + LOGGER.debug("No enemies visible - skipping hidden unit reposition evaluation"); + return; + } + + for (Entity entity : getEntitiesOwned()) { + if (!entity.isHidden()) { + continue; + } + + // Skip if reveal threshold is "never reveal" (index 0) + double revealThreshold = getBehaviorSettings().getHiddenUnitRevealValue(); + if (revealThreshold == 0.0) { + continue; + } + + evaluateHiddenUnitForRepositioning(entity, enemies); + } + } + + /** + * Evaluates a single hidden unit to determine if it should reveal and reposition. + * + * @param entity The hidden unit to evaluate + * @param enemies List of visible enemy entities + */ + private void evaluateHiddenUnitForRepositioning(Entity entity, List enemies) { + // Calculate current position damage + double currentDamage = calculateDamageFromCurrentPosition(entity); + + // Generate candidate reposition locations + List candidates = generateRepositionCandidates(entity, enemies); + + if (candidates.isEmpty()) { + LOGGER.debug("Hidden unit {} has no reposition candidates", entity.getDisplayName()); + return; + } + + // Find the best reposition damage + double bestRepositionDamage = 0; + CoordFacingCombo bestPosition = null; + + for (CoordFacingCombo candidate : candidates) { + double damage = calculateDamageFromHypotheticalPosition(entity, candidate, enemies); + if (damage > bestRepositionDamage) { + bestRepositionDamage = damage; + bestPosition = candidate; + } + } + + LOGGER.debug("Hidden unit {} reposition evaluation: currentDamage={}, bestRepositionDamage={}, " + + "threshold={}, bestPosition={}", + entity.getDisplayName(), + String.format("%.2f", currentDamage), + String.format("%.2f", bestRepositionDamage), + REPOSITION_DAMAGE_THRESHOLD, + bestPosition != null ? bestPosition.getCoords() : "none"); + + // Decision: reveal if repositioning gives significantly better damage + if (bestRepositionDamage > currentDamage * REPOSITION_DAMAGE_THRESHOLD) { + LOGGER.info("Hidden unit {} will reveal to reposition: {} -> {} (damage: {} -> {})", + entity.getDisplayName(), + entity.getPosition(), + bestPosition != null ? bestPosition.getCoords() : "unknown", + String.format("%.1f", currentDamage), + String.format("%.1f", bestRepositionDamage)); + + // Mark unit to reveal at start of movement phase + sendActivateHidden(entity.getId(), GamePhase.MOVEMENT); + } + } + + /** + * Calculates expected damage from the unit's current position using torso twist if needed. + * + * @param entity The entity to evaluate + * + * @return Expected damage from current position + */ + private double calculateDamageFromCurrentPosition(Entity entity) { + final Map ammoConservation = calcAmmoConservation(entity); + final FiringPlan plan = getFireControl(entity).getBestFiringPlan( + entity, getHonorUtil(), game, ammoConservation); + + if (plan == null || plan.isEmpty()) { + return 0; + } + return plan.getExpectedDamage(); + } + + /** + * Generates candidate positions for a hidden unit to reposition to. Focuses on positions that would improve firing + * angles toward enemies. + * + * @param entity The hidden unit + * @param enemies List of enemy entities to consider + * + * @return List of candidate positions with facings + */ + private List generateRepositionCandidates(Entity entity, List enemies) { + List candidates = new ArrayList<>(); + + // Get movement range - use walk MP for conservative estimate + int movementRange = entity.getWalkMP(); + if (movementRange <= 0) { + return candidates; + } + + // Cap the search range to avoid too many candidates + int searchRange = Math.min(movementRange, 6); + + Coords currentPos = entity.getPosition(); + if (currentPos == null) { + return candidates; + } + + // Find enemy concentration center for facing calculation + Coords enemyCenter = calculateEnemyCenter(enemies); + + // Generate positions within movement range + for (int radius = 1; radius <= searchRange; radius++) { + for (int dir = 0; dir < 6; dir++) { + Coords candidate = currentPos.translated(dir, radius); + + // Skip if off board or impassable + if (!game.getBoard().contains(candidate)) { + continue; + } + Hex hex = game.getBoard().getHex(candidate); + if (hex == null || hex.containsTerrain(Terrains.IMPASSABLE)) { + continue; + } + + // Calculate facing toward enemy center + int facingToEnemy = currentPos.direction(enemyCenter); + + // Add candidate with facing toward enemies + candidates.add(CoordFacingCombo.createCoordFacingCombo(candidate, facingToEnemy)); + + // Also try adjacent facings for flexibility + candidates.add(CoordFacingCombo.createCoordFacingCombo(candidate, (facingToEnemy + 1) % 6)); + candidates.add(CoordFacingCombo.createCoordFacingCombo(candidate, (facingToEnemy + 5) % 6)); + } + } + + return candidates; + } + + /** + * Calculates the center of mass of enemy positions. + * + * @param enemies List of enemy entities + * + * @return Coords representing the center of enemy positions + */ + private Coords calculateEnemyCenter(List enemies) { + if (enemies.isEmpty()) { + return new Coords(0, 0); + } + + int totalX = 0; + int totalY = 0; + int count = 0; + + for (Entity enemy : enemies) { + if (enemy.getPosition() != null) { + totalX += enemy.getPosition().getX(); + totalY += enemy.getPosition().getY(); + count++; + } + } + + if (count == 0) { + return new Coords(0, 0); + } + + return new Coords(totalX / count, totalY / count); + } + + /** + * Calculates expected damage from a hypothetical position against all enemies. + * + * @param entity The entity to evaluate + * @param position The hypothetical position and facing + * @param enemies List of enemy entities + * + * @return Expected damage from the hypothetical position + */ + private double calculateDamageFromHypotheticalPosition(Entity entity, CoordFacingCombo position, + List enemies) { + double maxDamage = 0; + + // Create hypothetical entity state at the new position + EntityState hypotheticalState = new EntityState(entity, position); + + for (Entity enemy : enemies) { + if (enemy.getPosition() == null || enemy.isDestroyed()) { + continue; + } + + try { + // Build firing plan parameters for this hypothetical situation + FiringPlanCalculationParameters guess = new Builder() + .buildGuess(entity, hypotheticalState, enemy, null, + (entity.getHeatCapacity() - entity.getHeat()) + 5, null); + + FiringPlan plan = getFireControl(entity).determineBestFiringPlan(guess); + if (plan != null) { + double damage = plan.getExpectedDamage(); + if (damage > maxDamage) { + maxDamage = damage; + } + } + } catch (Exception e) { + LOGGER.debug("Error calculating hypothetical damage for {} at {}: {}", + entity.getDisplayName(), position.getCoords(), e.getMessage()); + } + } + + return maxDamage; + } } diff --git a/megamek/src/megamek/client/bot/princess/commands/RevealCommand.java b/megamek/src/megamek/client/bot/princess/commands/RevealCommand.java new file mode 100644 index 00000000000..c130ccae7a0 --- /dev/null +++ b/megamek/src/megamek/client/bot/princess/commands/RevealCommand.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2025 The MegaMek Team. All Rights Reserved. + * + * This file is part of MegaMek. + * + * MegaMek is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License (GPL), + * version 3 or (at your option) any later version, + * as published by the Free Software Foundation. + * + * MegaMek is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * A copy of the GPL should have been included with this project; + * if not, see . + * + * NOTICE: The MegaMek organization is a non-profit group of volunteers + * creating free software for the BattleTech community. + * + * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks + * of The Topps Company, Inc. All Rights Reserved. + * + * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of + * InMediaRes Productions, LLC. + * + * MechWarrior Copyright Microsoft Corporation. MegaMek was created under + * Microsoft's "Game Content Usage Rules" + * and it is not endorsed by or + * affiliated with Microsoft. + */ +package megamek.client.bot.princess.commands; + +import megamek.client.bot.princess.Princess; +import megamek.common.units.Entity; +import megamek.server.commands.arguments.Arguments; + +/** + * Command to reveal all hidden units controlled by this bot. Useful for testing and debugging hidden unit behavior. + * + * @author MegaMek Team + */ +public class RevealCommand implements ChatCommand { + @Override + public void execute(Princess princess, Arguments arguments) { + int revealedCount = 0; + + for (Entity entity : princess.getEntitiesOwned()) { + if (entity.isHidden()) { + entity.setHidden(false); + princess.sendUpdateEntity(entity); + revealedCount++; + princess.sendChat("Revealed: " + entity.getDisplayName()); + } + } + + if (revealedCount == 0) { + princess.sendChat("No hidden units to reveal."); + } else { + princess.sendChat("Revealed " + revealedCount + " hidden unit(s)."); + } + } +} diff --git a/megamek/src/megamek/client/ui/dialogs/buttonDialogs/BotConfigDialog.java b/megamek/src/megamek/client/ui/dialogs/buttonDialogs/BotConfigDialog.java index 6d6445e150d..9c2c51e80e6 100644 --- a/megamek/src/megamek/client/ui/dialogs/buttonDialogs/BotConfigDialog.java +++ b/megamek/src/megamek/client/ui/dialogs/buttonDialogs/BotConfigDialog.java @@ -141,6 +141,7 @@ public class BotConfigDialog extends AbstractButtonDialog private final TipSlider herdingSlidebar = new TipSlider(SwingConstants.HORIZONTAL, 0, 10, 5); private final TipSlider selfPreservationSlidebar = new TipSlider(SwingConstants.HORIZONTAL, 0, 10, 5); private final TipSlider braverySlidebar = new TipSlider(SwingConstants.HORIZONTAL, 0, 10, 5); + private final TipSlider hiddenUnitRevealSlidebar = new TipSlider(SwingConstants.HORIZONTAL, 0, 10, 5); private final TipSlider antiCrowdingSlidebar = new TipSlider(SwingConstants.HORIZONTAL, 0, 10, 0); private final TipSlider favorHigherTMMSlidebar = new TipSlider(SwingConstants.HORIZONTAL, 0, 10, 0); private final TipSlider numberOfEnemiesToConsiderFacingSlidebar = new TipSlider(SwingConstants.HORIZONTAL, @@ -341,6 +342,13 @@ private JPanel behaviorSection() { "BotConfigDialog.braverySliderTitle")); panContent.add(Box.createVerticalStrut(7)); + panContent.add(buildSliderWithDynamicTitle(hiddenUnitRevealSlidebar, + Messages.getString("BotConfigDialog.hiddenUnitRevealSliderMin"), + Messages.getString("BotConfigDialog.hiddenUnitRevealSliderMax"), + Messages.getString("BotConfigDialog.hiddenUnitRevealTooltip"), + "BotConfigDialog.hiddenUnitRevealSliderTitle")); + panContent.add(Box.createVerticalStrut(7)); + panContent.add(buildSliderWithDynamicTitle(selfPreservationSlidebar, Messages.getString("BotConfigDialog.selfPreservationSliderMin"), Messages.getString("BotConfigDialog.selfPreservationSliderMax"), @@ -535,6 +543,7 @@ protected void updatePresetFields() { fallShameSlidebar.setValue(princessBehavior.getFallShameIndex()); herdingSlidebar.setValue(princessBehavior.getHerdMentalityIndex()); braverySlidebar.setValue(princessBehavior.getBraveryIndex()); + hiddenUnitRevealSlidebar.setValue(princessBehavior.getHiddenUnitRevealIndex()); antiCrowdingSlidebar.setValue(princessBehavior.getAntiCrowding()); favorHigherTMMSlidebar.setValue(princessBehavior.getFavorHigherTMM()); exclusiveHerdingCheck.setSelected(princessBehavior.isExclusiveHerding()); @@ -593,6 +602,7 @@ private boolean isChangedPreset() { chosenPreset.getFallShameIndex() != fallShameSlidebar.getValue() || chosenPreset.getHerdMentalityIndex() != herdingSlidebar.getValue() || chosenPreset.getBraveryIndex() != braverySlidebar.getValue() || + chosenPreset.getHiddenUnitRevealIndex() != hiddenUnitRevealSlidebar.getValue() || chosenPreset.getAntiCrowding() != antiCrowdingSlidebar.getValue() || chosenPreset.getFavorHigherTMM() != favorHigherTMMSlidebar.getValue() || chosenPreset.iAmAPirate() != iAmAPirateCheck.isSelected() || @@ -779,6 +789,7 @@ private void writePreset(String name) { newBehavior.setSelfPreservationIndex(selfPreservationSlidebar.getValue()); newBehavior.setHerdMentalityIndex(herdingSlidebar.getValue()); newBehavior.setBraveryIndex(braverySlidebar.getValue()); + newBehavior.setHiddenUnitRevealIndex(hiddenUnitRevealSlidebar.getValue()); newBehavior.setFavorHigherTMM(favorHigherTMMSlidebar.getValue()); newBehavior.setAntiCrowding(antiCrowdingSlidebar.getValue()); newBehavior.setNumberOfEnemiesToConsiderFacing(numberOfEnemiesToConsiderFacingSlidebar.getValue()); @@ -828,6 +839,7 @@ private void savePrincessProperties() { tempBehavior.setSelfPreservationIndex(selfPreservationSlidebar.getValue()); tempBehavior.setHerdMentalityIndex(herdingSlidebar.getValue()); tempBehavior.setBraveryIndex(braverySlidebar.getValue()); + tempBehavior.setHiddenUnitRevealIndex(hiddenUnitRevealSlidebar.getValue()); tempBehavior.setAntiCrowding(antiCrowdingSlidebar.getValue()); tempBehavior.setFavorHigherTMM(favorHigherTMMSlidebar.getValue()); tempBehavior.setNumberOfEnemiesToConsiderFacing(numberOfEnemiesToConsiderFacingSlidebar.getValue());