/** * Copyright (c) 2005-2017, KoLmafia development team * http://kolmafia.sourceforge.net/ * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * [1] Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * [2] Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * [3] Neither the name "KoLmafia" nor the names of its contributors may * be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package net.sourceforge.kolmafia.moods; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import net.sourceforge.kolmafia.AdventureResult; import net.sourceforge.kolmafia.KoLCharacter; import net.sourceforge.kolmafia.KoLConstants; import net.sourceforge.kolmafia.KoLmafia; import net.sourceforge.kolmafia.KoLmafiaCLI; import net.sourceforge.kolmafia.maximizer.Evaluator; import net.sourceforge.kolmafia.persistence.SkillDatabase; import net.sourceforge.kolmafia.preferences.Preferences; import net.sourceforge.kolmafia.request.UneffectRequest; import net.sourceforge.kolmafia.request.UseSkillRequest; import net.sourceforge.kolmafia.session.BreakfastManager; import net.sourceforge.kolmafia.utilities.StringUtilities; public class ManaBurnManager { public static final void burnExtraMana( final boolean isManualInvocation ) { if ( KoLmafia.refusesContinue() || KoLCharacter.inZombiecore() || KoLCharacter.getLimitmode() != null ) { return; } String nextBurnCast; float manaBurnTrigger = Preferences.getFloat( "manaBurningTrigger" ); if ( !isManualInvocation && KoLCharacter.getCurrentMP() < (int) (manaBurnTrigger * KoLCharacter.getMaximumMP()) ) { return; } boolean was = MoodManager.isExecuting; MoodManager.isExecuting = true; int currentMP = -1; while ( currentMP != KoLCharacter.getCurrentMP() && ( nextBurnCast = ManaBurnManager.getNextBurnCast() ) != null ) { currentMP = KoLCharacter.getCurrentMP(); KoLmafiaCLI.DEFAULT_SHELL.executeLine( nextBurnCast ); } MoodManager.isExecuting = was; } public static final void burnMana( int minimum ) { if ( KoLCharacter.inZombiecore() ) { return; } String nextBurnCast; boolean was = MoodManager.isExecuting; MoodManager.isExecuting = true; minimum = Math.max( 0, minimum ); int currentMP = -1; while ( currentMP != KoLCharacter.getCurrentMP() && ( nextBurnCast = ManaBurnManager.getNextBurnCast( minimum ) ) != null ) { currentMP = KoLCharacter.getCurrentMP(); KoLmafiaCLI.DEFAULT_SHELL.executeLine( nextBurnCast ); } MoodManager.isExecuting = was; } public static final String getNextBurnCast() { // Punt immediately if mana burning is disabled float manaBurnPreference = Preferences.getFloat( "manaBurningThreshold" ); if ( manaBurnPreference < 0.0f ) { return null; } float manaRecoverPreference = Preferences.getFloat( "mpAutoRecovery" ); int minimum = (int) ( Math.max( manaBurnPreference, manaRecoverPreference ) * (float) KoLCharacter.getMaximumMP() ) + 1; return ManaBurnManager.getNextBurnCast( minimum ); } private static final String getNextBurnCast( final int minimum ) { // Punt immediately if already burned enough or must recover MP int allowedMP = KoLCharacter.getCurrentMP() - minimum; if ( allowedMP <= 0 ) { return null; } // Pre-calculate possible breakfast/libram skill boolean onlyMood = !Preferences.getBoolean( "allowNonMoodBurning" ); String breakfast = Preferences.getBoolean( "allowSummonBurning" ) ? ManaBurnManager.considerBreakfastSkill( minimum ) : null; int summonThreshold = Preferences.getInteger( "manaBurnSummonThreshold" ); int durationLimit = Preferences.getInteger( "maxManaBurn" ) + KoLCharacter.getAdventuresLeft(); ManaBurn chosen = null; ArrayList<ManaBurn> burns = new ArrayList<ManaBurn>(); // Rather than maintain mood-related buffs only, maintain any // active effect that the character can auto-cast. Examine all // active effects in order from lowest duration to highest. for ( int i = 0; i < KoLConstants.activeEffects.size() && KoLmafia.permitsContinue(); ++i ) { AdventureResult currentEffect = (AdventureResult) KoLConstants.activeEffects.get( i ); String effectName = currentEffect.getName(); String skillName = UneffectRequest.effectToSkill( effectName ); // Only cast if the player knows the skill if ( !KoLCharacter.hasSkill( skillName ) ) { continue; } // Only cast if the MP cost is non-zero, since otherwise you'd // be in an infinite loop int skillId = SkillDatabase.getSkillId( skillName ); int mpCost = SkillDatabase.getMPConsumptionById( skillId ); if ( mpCost <= 0 ) { continue; } // Don't cast if you are restricted by your current class/skills if ( Evaluator.checkEffectConstraints( currentEffect.getEffectId() ) ) { continue; } int priority = Preferences.getInteger( "skillBurn" + skillId ) + 100; // skillBurnXXXX values offset by 100 so that missing prefs read // as 100% by default. // All skills that were previously hard-coded as unextendable are // now indicated by skillBurnXXXX = -100 in defaults.txt, so they // can be overridden if desired. int currentDuration = currentEffect.getCount(); int currentLimit = durationLimit * Math.min( 100, priority ) / 100; // If we already have 1000 turns more than the number // of turns the player has available, that's enough. // Also, if we have more than twice the turns of some // more expensive buff, save up for that one instead // of extending this one. if ( currentDuration >= currentLimit ) { continue; } // If the player wants to burn mana on summoning // skills, only do so if all potential effects have at // least 10 turns remaining. if ( breakfast != null && currentDuration >= summonThreshold ) { return breakfast; } // If the player only wants to cast buffs related to // their mood, then skip the buff if it's not in the // any of the player's moods. if ( onlyMood && !MoodManager.effectInMood( currentEffect ) ) { continue; } // If we don't have enough MP for this skill, consider // extending some cheaper effect - but only up to twice // the turns of this effect, so that a slow but steady // MP gain won't be used exclusively on the cheaper effect. if ( mpCost > allowedMP ) { durationLimit = Math.max( 10, Math.min( currentDuration * 2, durationLimit ) ); continue; } ManaBurn b = new ManaBurn( skillId, skillName, currentDuration, currentLimit ); if ( chosen == null ) { chosen = b; } burns.add( b ); breakfast = null; // we're definitely extending an effect } if ( chosen == null ) { // No buff found. Return possible breakfast/libram skill if ( breakfast != null || allowedMP < Preferences.getInteger( "lastChanceThreshold" ) ) { return breakfast; } // TODO: find the known but currently unused skill with the // highest skillBurnXXXX value (>0), and cast it. // Last chance: let the user specify something to do with this // MP that we can't find any other use for. Don't allow burn command // as there is no burn command that'll work without changing the amount // of MP spent String cmd = Preferences.getString( "lastChanceBurn" ); if ( cmd.length() == 0 || cmd.startsWith( "burn " ) ) { return null; } return StringUtilities.globalStringReplace( cmd, "#", String.valueOf( allowedMP ) ); } // Simulate casting all of the extendable skills in a balanced // manner, to determine the final count of the chosen skill - // rather than making multiple server requests. Iterator<ManaBurn> i = burns.iterator(); while ( i.hasNext() ) { ManaBurn b = i.next(); if ( !b.isCastable( allowedMP ) ) { i.remove(); continue; } allowedMP -= b.simulateCast(); Collections.sort( burns ); i = burns.iterator(); } return chosen.toString(); } private static final String considerBreakfastSkill( final int minimum ) { for ( int i = 0; i < UseSkillRequest.BREAKFAST_SKILLS.length; ++i ) { if ( !KoLCharacter.hasSkill( UseSkillRequest.BREAKFAST_SKILLS[ i ] ) ) { continue; } if ( UseSkillRequest.BREAKFAST_SKILLS[ i ].equals( "Pastamastery" ) && !KoLCharacter.canEat() ) { continue; } if ( UseSkillRequest.BREAKFAST_SKILLS[ i ].equals( "Advanced Cocktailcrafting" ) && !KoLCharacter.canDrink() ) { continue; } UseSkillRequest skill = UseSkillRequest.getInstance( UseSkillRequest.BREAKFAST_SKILLS[ i ] ); int maximumCast = skill.getMaximumCast(); if ( maximumCast == 0 ) { continue; } int availableMP = KoLCharacter.getCurrentMP() - minimum; int mpPerUse = SkillDatabase.getMPConsumptionById( skill.getSkillId() ); int castCount = Math.min( maximumCast, availableMP / mpPerUse ); if ( castCount > 0 ) { return "cast " + castCount + " " + UseSkillRequest.BREAKFAST_SKILLS[ i ]; } } return ManaBurnManager.considerLibramSummon( minimum ); } private static final String considerLibramSummon( final int minimum ) { int castCount = SkillDatabase.libramSkillCasts( KoLCharacter.getCurrentMP() - minimum ); if ( castCount <= 0 ) { return null; } List castable = BreakfastManager.getBreakfastLibramSkills(); int skillCount = castable.size(); if ( skillCount == 0 ) { return null; } int nextCast = Preferences.getInteger( "libramSummons" ); StringBuilder buf = new StringBuilder(); for ( int i = 0; i < skillCount; ++i ) { int thisCast = (castCount + skillCount - 1 - i) / skillCount; if ( thisCast <= 0 ) continue; buf.append( "cast " ); buf.append( thisCast ); buf.append( " " ); buf.append( (String) castable.get( (i + nextCast) % skillCount ) ); buf.append( ";" ); } return buf.toString(); } }