/** * 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.request; import java.util.HashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.sourceforge.kolmafia.AdventureResult; import net.sourceforge.kolmafia.BuffBotHome; import net.sourceforge.kolmafia.KoLAdventure; import net.sourceforge.kolmafia.KoLCharacter; import net.sourceforge.kolmafia.KoLConstants; import net.sourceforge.kolmafia.KoLConstants.MafiaState; import net.sourceforge.kolmafia.KoLmafia; import net.sourceforge.kolmafia.Modifiers; import net.sourceforge.kolmafia.PastaThrallData; import net.sourceforge.kolmafia.RequestLogger; import net.sourceforge.kolmafia.RequestThread; import net.sourceforge.kolmafia.SpecialOutfit; import net.sourceforge.kolmafia.Speculation; import net.sourceforge.kolmafia.moods.HPRestoreItemList; import net.sourceforge.kolmafia.moods.MoodManager; import net.sourceforge.kolmafia.moods.RecoveryManager; import net.sourceforge.kolmafia.objectpool.Concoction; import net.sourceforge.kolmafia.objectpool.EffectPool; import net.sourceforge.kolmafia.objectpool.ItemPool; import net.sourceforge.kolmafia.objectpool.SkillPool; import net.sourceforge.kolmafia.persistence.ConcoctionDatabase; import net.sourceforge.kolmafia.persistence.SkillDatabase; import net.sourceforge.kolmafia.preferences.Preferences; import net.sourceforge.kolmafia.request.CharPaneRequest.Companion; import net.sourceforge.kolmafia.session.ChoiceManager; import net.sourceforge.kolmafia.session.ContactManager; import net.sourceforge.kolmafia.session.ConsequenceManager; import net.sourceforge.kolmafia.session.DreadScrollManager; import net.sourceforge.kolmafia.session.EquipmentManager; import net.sourceforge.kolmafia.session.InventoryManager; import net.sourceforge.kolmafia.session.ResultProcessor; import net.sourceforge.kolmafia.utilities.InputFieldUtilities; import net.sourceforge.kolmafia.utilities.LockableListFactory; import net.sourceforge.kolmafia.utilities.StringUtilities; public class UseSkillRequest extends GenericRequest implements Comparable<UseSkillRequest> { private static final HashMap<String, UseSkillRequest> ALL_SKILLS = new HashMap<String, UseSkillRequest>(); private static final Pattern SKILLID_PATTERN = Pattern.compile( "whichskill=(\\d+)" ); private static final Pattern BOOKID_PATTERN = Pattern.compile( "preaction=(?:summon|combine)([^&]*)" ); private static final Pattern COUNT_PATTERN = Pattern.compile( "quantity=([\\*\\d,]+)" ); // <p>1 / 50 casts used today.</td> private static final Pattern LIMITED_PATTERN = Pattern.compile( "<p>(\\d+) / [\\d]+ casts used today\\.</td>", Pattern.DOTALL ); private static final Pattern SKILLZ_PATTERN = Pattern.compile( "rel=\\\"(\\d+)\\\".*?<span class=small>(.*?)</font></center></span>", Pattern.DOTALL ); public static final String[] BREAKFAST_SKILLS = { "Advanced Cocktailcrafting", "Advanced Saucecrafting", "Pastamastery", "Summon Crimbo Candy", "Lunch Break", "Spaghetti Breakfast", "Grab a Cold One", "Summon Holiday Fun!", "Summon Carrot", "Summon Kokomo Resort Pass", "Perfect Freeze", }; // These are skills where someone would not care if they are in-run, // generally because they do not cost MP public static final String[] BREAKFAST_ALWAYS_SKILLS = { "Summon Annoyance", "Communism!", }; public static final String[] TOME_SKILLS = { "Summon Snowcones", "Summon Stickers", "Summon Sugar Sheets", // Summon Clip Art requires extra parameters // "Summon Clip Art", "Summon Rad Libs", "Summon Smithsness" }; public static final String[] LIBRAM_SKILLS = { "Summon Candy Heart", "Summon Party Favor", "Summon Love Song", "Summon BRICKOs", "Summon Dice", "Summon Resolutions", "Summon Taffy", }; public static final String[] GRIMOIRE_SKILLS = { "Summon Hilarious Objects", "Summon Tasteful Items", "Summon Alice's Army Cards", "Summon Geeky Gifts", "Summon Confiscated Things", }; public static String lastUpdate = ""; public static int lastSkillUsed = -1; public static int lastSkillCount = 0; private final int skillId; private final boolean isBuff; private final String skillName; private String target; private int buffCount; private String countFieldId; private boolean isRunning; private int lastReduction = Integer.MAX_VALUE; private String lastStringForm = ""; // Tools for casting buffs. The lists are ordered from most bonus buff // turns provided by this tool to least. public static final BuffTool[] TAMER_TOOLS = new BuffTool[] { new BuffTool( ItemPool.FLAIL_OF_THE_SEVEN_ASPECTS, 15, false, null ), new BuffTool( ItemPool.CHELONIAN_MORNINGSTAR, 10, false, null ), new BuffTool( ItemPool.MACE_OF_THE_TORTOISE, 5, false, null ), new BuffTool( ItemPool.OUIJA_BOARD, 2, true, null ), new BuffTool( ItemPool.TURTLE_TOTEM, 0, false , null ), }; public static final BuffTool[] SAUCE_TOOLS = new BuffTool[] { new BuffTool( ItemPool.WINDSOR_PAN_OF_THE_SOURCE, 15, false, null ), new BuffTool( ItemPool.FRYING_BRAINPAN, 15, false, null ), new BuffTool( ItemPool.SAUCEPANIC, 10, false, null ), new BuffTool( ItemPool.SEVENTEEN_ALARM_SAUCEPAN, 10, false, null ), new BuffTool( ItemPool.OIL_PAN, 7, true, null ), new BuffTool( ItemPool.FIVE_ALARM_SAUCEPAN, 5, false, null ), new BuffTool( ItemPool.SAUCEPAN, 0, false, null ), }; public static final BuffTool[] THIEF_TOOLS = new BuffTool[] { new BuffTool( ItemPool.TRICKSTER_TRIKITIXA, 15, false, KoLCharacter.ACCORDION_THIEF ), new BuffTool( ItemPool.ZOMBIE_ACCORDION, 15, false, KoLCharacter.ACCORDION_THIEF ), new BuffTool( ItemPool.ALARM_ACCORDION, 15, false, KoLCharacter.ACCORDION_THIEF ), new BuffTool( ItemPool.PEACE_ACCORDION, 14, false, KoLCharacter.ACCORDION_THIEF ), new BuffTool( ItemPool.ACCORDIONOID_ROCCA, 13, false, KoLCharacter.ACCORDION_THIEF ), new BuffTool( ItemPool.PYGMY_CONCERTINETTE, 12, false, KoLCharacter.ACCORDION_THIEF ), new BuffTool( ItemPool.GHOST_ACCORDION, 11, false, KoLCharacter.ACCORDION_THIEF ), new BuffTool( ItemPool.SQUEEZEBOX_OF_THE_AGES, 10, false, KoLCharacter.ACCORDION_THIEF ), new BuffTool( ItemPool.SHAKESPEARES_SISTERS_ACCORDION, 10, false, KoLCharacter.ACCORDION_THIEF ), new BuffTool( ItemPool.AUTOCALLIOPE, 10, false, KoLCharacter.ACCORDION_THIEF ), new BuffTool( ItemPool.NON_EUCLIDEAN_NON_ACCORDION, 10, false, KoLCharacter.ACCORDION_THIEF ), new BuffTool( ItemPool.ACCORDION_OF_JORDION, 9, false, KoLCharacter.ACCORDION_THIEF ), new BuffTool( ItemPool.PENTATONIC_ACCORDION, 7, false, KoLCharacter.ACCORDION_THIEF ), new BuffTool( ItemPool.BONE_BANDONEON, 6, false, KoLCharacter.ACCORDION_THIEF ), new BuffTool( ItemPool.ANTIQUE_ACCORDION, 5, true, null ), new BuffTool( ItemPool.ACCORD_ION, 5, false, KoLCharacter.ACCORDION_THIEF ), new BuffTool( ItemPool.ACCORDION_FILE, 5, false, KoLCharacter.ACCORDION_THIEF ), new BuffTool( ItemPool.BAL_MUSETTE_ACCORDION, 5, false, KoLCharacter.ACCORDION_THIEF ), new BuffTool( ItemPool.CAJUN_ACCORDION, 5, false, KoLCharacter.ACCORDION_THIEF ), new BuffTool( ItemPool.QUIRKY_ACCORDION, 5, false, KoLCharacter.ACCORDION_THIEF ), new BuffTool( ItemPool.SKIPPERS_ACCORDION, 5, false, KoLCharacter.ACCORDION_THIEF ), new BuffTool( ItemPool.ROCK_N_ROLL_LEGEND, 5, false, KoLCharacter.ACCORDION_THIEF ), new BuffTool( ItemPool.GUANCERTINA, 4, false, KoLCharacter.ACCORDION_THIEF ), new BuffTool( ItemPool.MAMAS_SQUEEZEBOX, 3, false, KoLCharacter.ACCORDION_THIEF ), new BuffTool( ItemPool.BARITONE_ACCORDION, 2, false, KoLCharacter.ACCORDION_THIEF ), new BuffTool( ItemPool.CALAVERA_CONCERTINA, 2, false, KoLCharacter.ACCORDION_THIEF ), new BuffTool( ItemPool.BEER_BATTERED_ACCORDION, 1, false, KoLCharacter.ACCORDION_THIEF ), new BuffTool( ItemPool.STOLEN_ACCORDION, 0, false, KoLCharacter.ACCORDION_THIEF ), new BuffTool( ItemPool.TOY_ACCORDION, 0, false, null ), new BuffTool( ItemPool.AEROGEL_ACCORDION, 5, false, null ), }; public static final AdventureResult WIZARD_HAT = ItemPool.get( ItemPool.JEWEL_EYED_WIZARD_HAT, 1 ); public static final AdventureResult SHAKESPEARES_SISTERS_ACCORDION = ItemPool.get( ItemPool.SHAKESPEARES_SISTERS_ACCORDION, 1 ); public static final AdventureResult BASS_CLARINET = ItemPool.get( ItemPool.BASS_CLARINET, 1 ); public static final AdventureResult PLEXI_WATCH = ItemPool.get( ItemPool.PLEXIGLASS_POCKETWATCH, 1 ); public static final AdventureResult BRIM_BRACELET = ItemPool.get( ItemPool.BRIMSTONE_BRACELET, 1 ); public static final AdventureResult POCKET_SQUARE = ItemPool.get( ItemPool.POCKET_SQUARE, 1 ); public static final AdventureResult SOLITAIRE = ItemPool.get( ItemPool.STAINLESS_STEEL_SOLITAIRE, 1 ); public static final AdventureResult NAVEL_RING = ItemPool.get( ItemPool.NAVEL_RING, 1 ); public static final AdventureResult WIRE_BRACELET = ItemPool.get( ItemPool.WOVEN_BALING_WIRE_BRACELETS, 1 ); public static final AdventureResult BACON_BRACELET = ItemPool.get( ItemPool.BACONSTONE_BRACELET, 1 ); public static final AdventureResult BACON_EARRING = ItemPool.get( ItemPool.BACONSTONE_EARRING, 1 ); public static final AdventureResult SOLID_EARRING = ItemPool.get( ItemPool.SOLID_BACONSTONE_EARRING, 1 ); public static final AdventureResult EMBLEM_AKGYXOTH = ItemPool.get( ItemPool.EMBLEM_AKGYXOTH, 1 ); public static final AdventureResult SAUCEBLOB_BELT = ItemPool.get( ItemPool.SAUCEBLOB_BELT, 1 ); public static final AdventureResult JUJU_MOJO_MASK = ItemPool.get( ItemPool.JUJU_MOJO_MASK, 1 ); private static final AdventureResult[] AVOID_REMOVAL = new AdventureResult[] { UseSkillRequest.BASS_CLARINET, // -3 UseSkillRequest.BRIM_BRACELET, // -3 UseSkillRequest.PLEXI_WATCH, // -3 UseSkillRequest.POCKET_SQUARE, // -3 UseSkillRequest.SOLITAIRE, // -2 UseSkillRequest.SHAKESPEARES_SISTERS_ACCORDION, // -1 or -2 UseSkillRequest.WIZARD_HAT, // -1 UseSkillRequest.NAVEL_RING, // -1 UseSkillRequest.WIRE_BRACELET, // -1 UseSkillRequest.BACON_BRACELET, // -1, discontinued item UseSkillRequest.BACON_EARRING, // -1 UseSkillRequest.SOLID_EARRING, // -1 UseSkillRequest.EMBLEM_AKGYXOTH, // -1 // Removing the following may lose a buff UseSkillRequest.JUJU_MOJO_MASK, }; // The number of items at the end of AVOID_REMOVAL that are simply // there to avoid removal - there's no point in equipping them // temporarily during casting: private static final int AVOID_REMOVAL_ONLY = 1; // Other known MP cost items: // Vile Vagrant Vestments (-5) - unlikely to be equippable during Ronin. // Idol of Ak'gyxoth (-1) - off-hand, would require special handling. private UseSkillRequest( final String skillName ) { super( UseSkillRequest.chooseURL( skillName ) ); this.skillId = SkillDatabase.getSkillId( skillName ); if ( this.skillId == -1 ) { RequestLogger.printLine( "Unrecognized skill: " + skillName ); this.skillName = skillName; this.isBuff = false; } else { this.skillName = SkillDatabase.getSkillName( this.skillId ); this.isBuff = SkillDatabase.isBuff( this.skillId ); } this.target = null; this.addFormFields(); } private static String chooseURL( final String skillName ) { if ( SkillDatabase.isBookshelfSkill( skillName ) ) { return "campground.php"; } return "runskillz.php"; } private void addFormFields() { switch ( this.skillId ) { case SkillPool.SNOWCONE: this.addFormField( "preaction", "summonsnowcone" ); break; case SkillPool.STICKER: this.addFormField( "preaction", "summonstickers" ); break; case SkillPool.SUGAR: this.addFormField( "preaction", "summonsugarsheets" ); break; case SkillPool.CLIP_ART: this.addFormField( "preaction", "combinecliparts" ); break; case SkillPool.RAD_LIB: this.addFormField( "preaction", "summonradlibs" ); break; case SkillPool.SMITHSNESS: this.addFormField( "preaction", "summonsmithsness" ); break; case SkillPool.HILARIOUS: this.addFormField( "preaction", "summonhilariousitems" ); break; case SkillPool.TASTEFUL: this.addFormField( "preaction", "summonspencersitems" ); break; case SkillPool.CARDS: this.addFormField( "preaction", "summonaa" ); break; case SkillPool.GEEKY: this.addFormField( "preaction", "summonthinknerd" ); break; case SkillPool.CANDY_HEART: this.addFormField( "preaction", "summoncandyheart" ); break; case SkillPool.PARTY_FAVOR: this.addFormField( "preaction", "summonpartyfavor" ); break; case SkillPool.LOVE_SONG: this.addFormField( "preaction", "summonlovesongs" ); break; case SkillPool.BRICKOS: this.addFormField( "preaction", "summonbrickos" ); break; case SkillPool.DICE: this.addFormField( "preaction", "summongygax" ); break; case SkillPool.RESOLUTIONS: this.addFormField( "preaction", "summonresolutions" ); break; case SkillPool.TAFFY: this.addFormField( "preaction", "summontaffy" ); break; case SkillPool.CONFISCATOR: this.addFormField( "preaction", "summonconfiscators" ); break; default: this.addFormField( "action", "Skillz" ); this.addFormField( "whichskill", String.valueOf( this.skillId ) ); this.addFormField( "ajax", "1" ); break; } } public void setTarget( final String target ) { this.countFieldId = "quantity"; if ( this.isBuff ) { if ( target == null || target.trim().length() == 0 || target.equals( KoLCharacter.getPlayerId() ) || target.equals( KoLCharacter.getUserName() ) ) { this.target = null; this.addFormField( "targetplayer", KoLCharacter.getPlayerId() ); } else { this.target = ContactManager.getPlayerName( target ); this.addFormField( "targetplayer", ContactManager.getPlayerId( target ) ); } } else { this.target = null; } } public void setBuffCount( int buffCount ) { int maxPossible = 0; if ( SkillDatabase.isSoulsauceSkill( skillId ) ) { maxPossible = KoLCharacter.getSoulsauce() / SkillDatabase.getSoulsauceCost( skillId ); } else if ( SkillDatabase.isThunderSkill( skillId ) ) { maxPossible = KoLCharacter.getThunder() / SkillDatabase.getThunderCost( skillId ); } else if ( SkillDatabase.isRainSkill( skillId ) ) { maxPossible = KoLCharacter.getRain() / SkillDatabase.getRainCost( skillId ); } else if ( SkillDatabase.isLightningSkill( skillId ) ) { maxPossible = KoLCharacter.getLightning() / SkillDatabase.getLightningCost( skillId ); } else { int mpCost = SkillDatabase.getMPConsumptionById( this.skillId ); int availableMP = KoLCharacter.getCurrentMP(); if ( mpCost == 0 ) { maxPossible = this.getMaximumCast(); } else if ( SkillDatabase.isLibramSkill( this.skillId ) ) { maxPossible = SkillDatabase.libramSkillCasts( availableMP ); } else { maxPossible = Math.min( this.getMaximumCast(), availableMP / mpCost ); } } if ( buffCount < 1 ) { buffCount += maxPossible; } else if ( buffCount == Integer.MAX_VALUE ) { buffCount = maxPossible; } this.buffCount = buffCount; } public int compareTo( final UseSkillRequest o ) { if ( o == null || !( o instanceof UseSkillRequest ) ) { return -1; } int mpDifference = SkillDatabase.getMPConsumptionById( this.skillId ) - SkillDatabase.getMPConsumptionById( ( (UseSkillRequest) o ).skillId ); return mpDifference != 0 ? mpDifference : this.skillName.compareToIgnoreCase( ( (UseSkillRequest) o ).skillName ); } public int getSkillId() { return this.skillId; } public String getSkillName() { return this.skillName; } public int getMaximumCast() { int maximumCast = Integer.MAX_VALUE; boolean canCastHoboSong = KoLCharacter.getClassType() == KoLCharacter.ACCORDION_THIEF && KoLCharacter.getLevel() > 14; switch ( this.skillId ) { // The Smile of Mr. A can be used five times per day per Golden // Mr. Accessory you own case SkillPool.SMILE_OF_MR_A: maximumCast = Preferences.getInteger( "goldenMrAccessories" ) * 5 - Preferences.getInteger( "_smilesOfMrA" ); break; // Vent Rage Gland can be used once per day case SkillPool.RAGE_GLAND: maximumCast = Preferences.getBoolean( "rageGlandVented" ) ? 0 : 1; break; // You can take a Lunch Break once a day case SkillPool.LUNCH_BREAK: maximumCast = Preferences.getBoolean( "_lunchBreak" ) ? 0 : 1; break; // Spaghetti Breakfast once a day case SkillPool.SPAGHETTI_BREAKFAST: maximumCast = Preferences.getBoolean( "_spaghettiBreakfast" ) ? 0 : 1; break; // Grab a Cold One once a day case SkillPool.GRAB_A_COLD_ONE: maximumCast = Preferences.getBoolean( "_coldOne" ) ? 0 : 1; break; // That's Not a Knife once a day case SkillPool.THATS_NOT_A_KNIFE: maximumCast = Preferences.getBoolean( "_discoKnife" ) ? 0 : 1; break; // Summon "Boner Battalion" can be used once per day case SkillPool.SUMMON_BONERS: maximumCast = Preferences.getBoolean( "_bonersSummoned" ) ? 0 : 1; break; case SkillPool.REQUEST_SANDWICH: maximumCast = Preferences.getBoolean( "_requestSandwichSucceeded" ) ? 0 : 1; break; // Tomes can be used three times per day. In aftercore, each tome can be used 3 times per day. case SkillPool.SNOWCONE: maximumCast = KoLCharacter.canInteract() ? Math.max( 3 - Preferences.getInteger( "_snowconeSummons" ), 0 ) : Math.max( 3 - Preferences.getInteger( "tomeSummons" ), 0 ); break; case SkillPool.STICKER: maximumCast = KoLCharacter.canInteract() ? Math.max( 3 - Preferences.getInteger( "_stickerSummons" ), 0 ) : Math.max( 3 - Preferences.getInteger( "tomeSummons" ), 0 ); break; case SkillPool.SUGAR: maximumCast = KoLCharacter.canInteract() ? Math.max( 3 - Preferences.getInteger( "_sugarSummons" ), 0 ) : Math.max( 3 - Preferences.getInteger( "tomeSummons" ), 0 ); break; case SkillPool.CLIP_ART: maximumCast = KoLCharacter.canInteract() ? Math.max( 3 - Preferences.getInteger( "_clipartSummons" ), 0 ) : Math.max( 3 - Preferences.getInteger( "tomeSummons" ), 0 ); break; case SkillPool.RAD_LIB: maximumCast = KoLCharacter.canInteract() ? Math.max( 3 - Preferences.getInteger( "_radlibSummons" ), 0 ) : Math.max( 3 - Preferences.getInteger( "tomeSummons" ), 0 ); break; case SkillPool.SMITHSNESS: maximumCast = KoLCharacter.canInteract() ? Math.max( 3 - Preferences.getInteger( "_smithsnessSummons" ), 0 ) : Math.max( 3 - Preferences.getInteger( "tomeSummons" ), 0 ); break; // Grimoire items can only be summoned once per day. case SkillPool.HILARIOUS: maximumCast = Math.max( 1 - Preferences.getInteger( "grimoire1Summons" ), 0 ); break; case SkillPool.TASTEFUL: maximumCast = Math.max( 1 - Preferences.getInteger( "grimoire2Summons" ), 0 ); break; case SkillPool.CARDS: maximumCast = Math.max( 1 - Preferences.getInteger( "grimoire3Summons" ), 0 ); break; case SkillPool.GEEKY: maximumCast = Math.max( 1 - Preferences.getInteger( "_grimoireGeekySummons" ), 0 ); break; case SkillPool.CONFISCATOR: maximumCast = Math.max( 1 - Preferences.getInteger( "_grimoireConfiscatorSummons" ), 0 ); break; // You can summon Crimbo candy once a day case SkillPool.CRIMBO_CANDY: maximumCast = Math.max( 1 - Preferences.getInteger( "_candySummons" ), 0 ); break; // Rainbow Gravitation can be cast 3 times per day. Each // casting consumes five elemental wads and a twinkly wad case SkillPool.RAINBOW_GRAVITATION: maximumCast = Math.max( 3 - Preferences.getInteger( "prismaticSummons" ), 0 ); maximumCast = Math.min( InventoryManager.getAccessibleCount( ItemPool.COLD_WAD ), maximumCast ); maximumCast = Math.min( InventoryManager.getAccessibleCount( ItemPool.HOT_WAD ), maximumCast ); maximumCast = Math.min( InventoryManager.getAccessibleCount( ItemPool.SLEAZE_WAD ), maximumCast ); maximumCast = Math.min( InventoryManager.getAccessibleCount( ItemPool.SPOOKY_WAD ), maximumCast ); maximumCast = Math.min( InventoryManager.getAccessibleCount( ItemPool.STENCH_WAD ), maximumCast ); maximumCast = Math.min( InventoryManager.getAccessibleCount( ItemPool.TWINKLY_WAD ), maximumCast ); break; case SkillPool.PASTAMASTERY: maximumCast = ( Preferences.getInteger( "noodleSummons" ) == 0 ) ? 1 : 0; break; // Canticle of Carboloading can be cast once per day. case SkillPool.CARBOLOADING: maximumCast = Preferences.getBoolean( "_carboLoaded" ) ? 0 : 1; break; case SkillPool.ADVANCED_SAUCECRAFTING: maximumCast = ( Preferences.getInteger( "reagentSummons" ) == 0 ) ? 1 : 0; break; case SkillPool.ADVANCED_COCKTAIL: maximumCast = ( Preferences.getInteger( "cocktailSummons" ) == 0 ) ? 1 : 0; break; case SkillPool.THINGFINDER: maximumCast = canCastHoboSong ? Math.max( 10 - Preferences.getInteger( "_thingfinderCasts" ), 0 ) : 0; break; case SkillPool.BENETTONS: maximumCast = canCastHoboSong ? Math.max( 10 - Preferences.getInteger( "_benettonsCasts" ), 0 ) : 0; break; case SkillPool.ELRONS: maximumCast = canCastHoboSong ? Math.max( 10 - Preferences.getInteger( "_elronsCasts" ), 0 ) : 0; break; case SkillPool.COMPANIONSHIP: maximumCast = canCastHoboSong ? Math.max( 10 - Preferences.getInteger( "_companionshipCasts" ), 0 ) : 0; break; case SkillPool.PRECISION: maximumCast = canCastHoboSong ? Math.max( 10 - Preferences.getInteger( "_precisionCasts" ), 0 ) : 0; break; case SkillPool.DONHOS: maximumCast = Math.max( 50 - Preferences.getInteger( "_donhosCasts" ), 0 ); break; case SkillPool.INIGOS: maximumCast = Math.max( 5 - Preferences.getInteger( "_inigosCasts" ), 0 ); break; // Avatar of Boris skill case SkillPool.DEMAND_SANDWICH: maximumCast = Math.max( 3 - Preferences.getInteger( "_demandSandwich" ), 0 ); break; // Zombie Master skills case SkillPool.SUMMON_MINION: maximumCast = KoLCharacter.getAvailableMeat() / 100; break; case SkillPool.SUMMON_HORDE: maximumCast = KoLCharacter.getAvailableMeat() / 1000; break; // Avatar of Jarlsberg skills case SkillPool.CONJURE_EGGS: maximumCast = Preferences.getBoolean( "_jarlsEggsSummoned" ) ? 0 : 1; break; case SkillPool.CONJURE_DOUGH: maximumCast = Preferences.getBoolean( "_jarlsDoughSummoned" ) ? 0 : 1; break; case SkillPool.CONJURE_VEGGIES: maximumCast = Preferences.getBoolean( "_jarlsVeggiesSummoned" ) ? 0 : 1; break; case SkillPool.CONJURE_CHEESE: maximumCast = Preferences.getBoolean( "_jarlsCheeseSummoned" ) ? 0 : 1; break; case SkillPool.CONJURE_MEAT: maximumCast = Preferences.getBoolean( "_jarlsMeatSummoned" ) ? 0 : 1; break; case SkillPool.CONJURE_POTATO: maximumCast = Preferences.getBoolean( "_jarlsPotatoSummoned" ) ? 0 : 1; break; case SkillPool.CONJURE_CREAM: maximumCast = Preferences.getBoolean( "_jarlsCreamSummoned" ) ? 0 : 1; break; case SkillPool.CONJURE_FRUIT: maximumCast = Preferences.getBoolean( "_jarlsFruitSummoned" ) ? 0 : 1; break; case SkillPool.EGGMAN: boolean haveEgg = KoLConstants.inventory.contains( ItemPool.get( ItemPool.COSMIC_EGG, 1 ) ); boolean eggActive = KoLCharacter.getCompanion() == Companion.EGGMAN; maximumCast = ( haveEgg && !eggActive ) ? 1 : 0; break; case SkillPool.RADISH_HORSE: boolean haveVeggie = KoLConstants.inventory.contains( ItemPool.get( ItemPool.COSMIC_VEGETABLE, 1 ) ); boolean radishActive = KoLCharacter.getCompanion() == Companion.RADISH; maximumCast = ( haveVeggie && !radishActive ) ? 1 : 0; break; case SkillPool.HIPPOTATO: boolean havePotato = KoLConstants.inventory.contains( ItemPool.get( ItemPool.COSMIC_POTATO, 1 ) ); boolean hippoActive = KoLCharacter.getCompanion() == Companion.HIPPO; maximumCast = ( havePotato && !hippoActive ) ? 1 : 0; break; case SkillPool.CREAMPUFF: boolean haveCream = KoLConstants.inventory.contains( ItemPool.get( ItemPool.COSMIC_CREAM, 1 ) ); boolean creampuffActive = KoLCharacter.getCompanion() == Companion.CREAM; maximumCast = ( haveCream && !creampuffActive ) ? 1 : 0; break; case SkillPool.DEEP_VISIONS: maximumCast = KoLCharacter.getMaximumHP() >= 500 ? 1 : 0; break; case SkillPool.WAR_BLESSING: maximumCast = ( KoLCharacter.getBlessingLevel() != -1 || KoLCharacter.getBlessingType() == KoLCharacter.WAR_BLESSING ) ? 1 : 0; break; case SkillPool.SHE_WHO_WAS_BLESSING: maximumCast = ( KoLCharacter.getBlessingLevel() != -1 || KoLCharacter.getBlessingType() == KoLCharacter.SHE_WHO_WAS_BLESSING ) ? 1 : 0; break; case SkillPool.STORM_BLESSING: maximumCast = ( KoLCharacter.getBlessingLevel() != -1 || KoLCharacter.getBlessingType() == KoLCharacter.STORM_BLESSING ) ? 1 : 0; break; case SkillPool.SPIRIT_BOON: maximumCast = KoLCharacter.getBlessingLevel() != 0 ? Integer.MAX_VALUE : 0; break; case SkillPool.TURTLE_POWER: maximumCast = KoLCharacter.getBlessingLevel() == 3 && !Preferences.getBoolean( "_turtlePowerCast" ) ? 1 : 0; break; case SkillPool.PSYCHOKINETIC_HUG: maximumCast = Preferences.getBoolean( "_psychokineticHugUsed" ) ? 0 : 1; break; case SkillPool.MANAGERIAL_MANIPULATION: maximumCast = Preferences.getBoolean( "_managerialManipulationUsed" ) ? 0 : 1; break; case SkillPool.THROW_PARTY: maximumCast = Preferences.getBoolean( "_petePartyThrown" ) ? 0 : 1; break; case SkillPool.INCITE_RIOT: maximumCast = Preferences.getBoolean( "_peteRiotIncited" ) ? 0 : 1; break; case SkillPool.SUMMON_ANNOYANCE: if ( Preferences.getInteger( "summonAnnoyanceCost" ) == 11 ) { // If we made it this far, you should have the skill. // Update its cost. GenericRequest req = new GenericRequest( "desc_skill.php?whichskill=" + this.skillId + "&self=true" ); RequestThread.postRequest( req ); } if ( Preferences.getBoolean( "_summonAnnoyanceUsed" ) ) { maximumCast = 0; break; } if ( Preferences.getInteger( "availableSwagger" ) < Preferences.getInteger( "summonAnnoyanceCost" ) ) { maximumCast = 0; break; } maximumCast = 1; break; case SkillPool.PIRATE_BELLOW: maximumCast = Preferences.getBoolean( "_pirateBellowUsed" ) ? 0 : 1; break; case SkillPool.HOLIDAY_FUN: maximumCast = Preferences.getBoolean( "_holidayFunUsed" ) ? 0 : 1; break; case SkillPool.SUMMON_CARROT: maximumCast = Preferences.getBoolean( "_summonCarrotUsed" ) ? 0 : 1; break; case SkillPool.SUMMON_KOKOMO_RESORT_PASS: maximumCast = Preferences.getBoolean( "_summonResortPassUsed" ) ? 0 : 1; break; case SkillPool.CALCULATE_THE_UNIVERSE: if ( KoLCharacter.getAdventuresLeft() == 0 ) { maximumCast = 0; break; } if ( Preferences.getInteger( "skillLevel144" ) == 0 ) { // If the skill is being cast, then the limit must be at least 1 Preferences.setInteger( "skillLevel144", 1 ); } maximumCast = Preferences.getInteger( "skillLevel144" ) > Preferences.getInteger( "_universeCalculated" ) ? 1 : 0; break; case SkillPool.ANCESTRAL_RECALL: maximumCast = Math.min( 10 - Preferences.getInteger( "_ancestralRecallCasts" ), InventoryManager.getAccessibleCount( ItemPool.BLUE_MANA ) ); break; case SkillPool.DARK_RITUAL: maximumCast = InventoryManager.getAccessibleCount( ItemPool.BLACK_MANA ); break; case SkillPool.PERFECT_FREEZE: maximumCast = Preferences.getBoolean( "_perfectFreezeUsed" ) ? 0 : 1; break; case SkillPool.COMMUNISM: maximumCast = Preferences.getBoolean( "_communismUsed" ) ? 0 : 1; break; case SkillPool.BOW_LEGGED_SWAGGER: maximumCast = Preferences.getBoolean( "_bowleggedSwaggerUsed" ) ? 0 : 1; break; case SkillPool.BEND_HELL: maximumCast = Preferences.getBoolean( "_bendHellUsed" ) ? 0 : 1; break; case SkillPool.STEELY_EYED_SQUINT: maximumCast = Preferences.getBoolean( "_steelyEyedSquintUsed" ) ? 0 : 1; break; case SkillPool.INTERNAL_SODA_MACHINE: int meatLimit = KoLCharacter.getAvailableMeat() / 20; int mpLimit = (int) Math.ceil( ( KoLCharacter.getMaximumMP() - KoLCharacter.getCurrentMP() ) / 10.0 ); maximumCast = Math.min( meatLimit, mpLimit ); break; case SkillPool.CECI_CHAPEAU: maximumCast = Preferences.getBoolean( "_ceciHatUsed" ) ? 0 : 1; break; case SkillPool.EVOKE_ELDRITCH_HORROR: maximumCast = Preferences.getBoolean( "_eldritchHorrorEvoked" ) ? 0 : 1; break; case SkillPool.STACK_LUMPS: maximumCast = 1; break; } return maximumCast; } @Override public String toString() { if ( this.lastReduction == KoLCharacter.getManaCostAdjustment() && !SkillDatabase.isLibramSkill( this.skillId ) ) { return this.lastStringForm; } this.lastReduction = KoLCharacter.getManaCostAdjustment(); int mpCost = SkillDatabase.getMPConsumptionById( this.skillId ); int advCost = SkillDatabase.getAdventureCost( this.skillId ); int soulCost = SkillDatabase.getSoulsauceCost( this.skillId ); int thunderCost = SkillDatabase.getThunderCost( this.skillId ); int rainCost = SkillDatabase.getRainCost( skillId ); int lightningCost = SkillDatabase.getLightningCost( skillId ); int numCosts = 0; int itemCost = 0; StringBuilder costString = new StringBuilder(); costString.append( this.skillName ); costString.append( " (" ); if ( advCost > 0 ) { costString.append( advCost ); costString.append( " adv" ); numCosts++; } if ( soulCost > 0 ) { if ( numCosts > 0 ) { costString.append( ", " ); } costString.append( soulCost ); costString.append( " soulsauce" ); numCosts++; } if ( this.skillId == SkillPool.SUMMON_ANNOYANCE ) { if ( numCosts > 0 ) { costString.append( ", " ); } costString.append( Preferences.getInteger( "summonAnnoyanceCost" ) ); costString.append( " swagger" ); numCosts++; } if ( this.skillId == SkillPool.HEALING_SALVE ) { if ( numCosts > 0 ) { costString.append( ", " ); } costString.append( "1 white mana" ); itemCost++; numCosts++; } else if ( this.skillId == SkillPool.DARK_RITUAL ) { if ( numCosts > 0 ) { costString.append( ", " ); } costString.append( "1 black mana" ); itemCost++; numCosts++; } else if ( this.skillId == SkillPool.LIGHTNING_BOLT_CARD ) { if ( numCosts > 0 ) { costString.append( ", " ); } costString.append( "1 red mana" ); itemCost++; numCosts++; } else if ( this.skillId == SkillPool.GIANT_GROWTH ) { if ( numCosts > 0 ) { costString.append( ", " ); } costString.append( "1 green mana" ); itemCost++; numCosts++; } else if ( this.skillId == SkillPool.ANCESTRAL_RECALL ) { if ( numCosts > 0 ) { costString.append( ", " ); } costString.append( "1 blue mana" ); itemCost++; numCosts++; } if ( thunderCost > 0 ) { if ( numCosts > 0 ) { costString.append( ", " ); } costString.append( thunderCost ); costString.append( " dB of thunder" ); numCosts++; } if ( rainCost > 0 ) { if ( numCosts > 0 ) { costString.append( ", " ); } costString.append( rainCost ); costString.append( " drops of rain" ); numCosts++; } if ( lightningCost > 0 ) { if ( numCosts > 0 ) { costString.append( ", " ); } costString.append( lightningCost ); costString.append( " bolts of lightning" ); numCosts++; } if ( mpCost > 0 || ( advCost == 0 && soulCost == 0 && this.skillId != SkillPool.SUMMON_ANNOYANCE && thunderCost == 0 && rainCost == 0 && lightningCost == 0 && itemCost == 0 ) ) { if ( numCosts > 0 ) { costString.append( ", " ); } costString.append( mpCost ); costString.append( " mp" ); } costString.append( ")" ); this.lastStringForm = costString.toString(); return this.lastStringForm; } private static final boolean canSwitchToItem( final AdventureResult item ) { return !KoLCharacter.hasEquipped( item ) && EquipmentManager.canEquip( item.getName() ) && InventoryManager.hasItem( item, false ); } public static final void optimizeEquipment( final int skillId ) { boolean isBuff = SkillDatabase.isBuff( skillId ); if ( isBuff ) { if ( SkillDatabase.isTurtleTamerBuff( skillId ) ) { UseSkillRequest.prepareTool( UseSkillRequest.TAMER_TOOLS, skillId ); } else if ( SkillDatabase.isSaucerorBuff( skillId ) ) { UseSkillRequest.prepareTool( UseSkillRequest.SAUCE_TOOLS, skillId ); } else if ( SkillDatabase.isAccordionThiefSong( skillId ) ) { UseSkillRequest.prepareTool( UseSkillRequest.THIEF_TOOLS, skillId ); } } if ( Preferences.getBoolean( "switchEquipmentForBuffs" ) ) { UseSkillRequest.reduceManaConsumption( skillId ); } } private static final boolean isValidSwitch( final int slotId, final AdventureResult newItem, final int skillId ) { AdventureResult item = EquipmentManager.getEquipment( slotId ); if ( item.equals( EquipmentRequest.UNEQUIP ) ) return true; for ( int i = 0; i < UseSkillRequest.AVOID_REMOVAL.length; ++i ) { if ( item.equals( UseSkillRequest.AVOID_REMOVAL[ i ] ) ) { return false; } } Speculation spec = new Speculation(); spec.equip( slotId, newItem ); int[] predictions = spec.calculate().predict(); // Make sure we do not lose mp in the switch if ( KoLCharacter.getCurrentMP() > predictions[ Modifiers.BUFFED_MP ] ) { return false; } // Make sure we do not reduce max hp in the switch, to avoid loops when casting a heal if ( KoLCharacter.getMaximumHP() > predictions[ Modifiers.BUFFED_HP ] ) { return false; } // Don't allow if we'd lose a song in the switch Modifiers mods = spec.getModifiers(); int predictedSongLimit = 3 + (int) mods.get( Modifiers.ADDITIONAL_SONG ) + ( mods.getBoolean( Modifiers.ADDITIONAL_SONG ) ? 1 : 0 ); int predictedSongsNeeded = UseSkillRequest.songsActive() + ( UseSkillRequest.newSong( skillId ) ? 1 : 0 ); if ( predictedSongsNeeded > predictedSongLimit ) { return false; } return true; } private static final int attemptSwitch( final int skillId, final AdventureResult item, final boolean slot1Allowed, final boolean slot2Allowed, final boolean slot3Allowed ) { if ( slot3Allowed ) { ( new EquipmentRequest( item, EquipmentManager.ACCESSORY3 ) ).run(); return EquipmentManager.ACCESSORY3; } if ( slot2Allowed ) { ( new EquipmentRequest( item, EquipmentManager.ACCESSORY2 ) ).run(); return EquipmentManager.ACCESSORY2; } if ( slot1Allowed ) { ( new EquipmentRequest( item, EquipmentManager.ACCESSORY1 ) ).run(); return EquipmentManager.ACCESSORY1; } return -1; } private static final void reduceManaConsumption( final int skillId ) { int mpCost = SkillDatabase.getMPConsumptionById( skillId ); // Never bother trying to reduce mana consumption when casting // expensive skills or a libram skill if ( mpCost > 50 || SkillDatabase.isLibramSkill( skillId ) || mpCost == 0 ) { return; } // MP is cheap in aftercore, so save server hits if ( KoLCharacter.canInteract() ) { return; } // Try items for ( int i = 0; i < UseSkillRequest.AVOID_REMOVAL.length - AVOID_REMOVAL_ONLY; ++i ) { // If you can't reduce cost further, stop if ( mpCost == 1 || KoLCharacter.currentNumericModifier( Modifiers.MANA_COST ) <= -3 ) { return; } // If you haven't got it or can't wear it, don't evaluate further if ( !UseSkillRequest.canSwitchToItem( UseSkillRequest.AVOID_REMOVAL[ i ] ) ) { continue; } // If you won't lose max hp, current mp, or songs, use it int slot = EquipmentManager.itemIdToEquipmentType( UseSkillRequest.AVOID_REMOVAL[ i ].getItemId() ); if ( slot == EquipmentManager.ACCESSORY1 ) { // First determine which slots are available for switching in // MP reduction items. This has do be done inside the loop now // that max HP/MP prediction is done, since two changes that are // individually harmless might add up to a loss of points. boolean slot1Allowed = UseSkillRequest.isValidSwitch( EquipmentManager.ACCESSORY1, UseSkillRequest.AVOID_REMOVAL[ i ], skillId ); boolean slot2Allowed = UseSkillRequest.isValidSwitch( EquipmentManager.ACCESSORY2, UseSkillRequest.AVOID_REMOVAL[ i ], skillId ); boolean slot3Allowed = UseSkillRequest.isValidSwitch( EquipmentManager.ACCESSORY3, UseSkillRequest.AVOID_REMOVAL[ i ], skillId ); UseSkillRequest.attemptSwitch( skillId, UseSkillRequest.AVOID_REMOVAL[ i ], slot1Allowed, slot2Allowed, slot3Allowed ); } else { if ( UseSkillRequest.isValidSwitch( slot, UseSkillRequest.AVOID_REMOVAL[ i ], skillId ) ) { ( new EquipmentRequest( UseSkillRequest.AVOID_REMOVAL[ i ], slot ) ).run(); } } // Cost may have changed mpCost = SkillDatabase.getMPConsumptionById( skillId ); } } public static final int songLimit() { int rv = 3; if ( KoLCharacter.currentBooleanModifier( Modifiers.FOUR_SONGS ) ) { ++rv; } rv += KoLCharacter.currentNumericModifier( Modifiers.ADDITIONAL_SONG ); return rv; } private static final int songsActive() { int count = 0; AdventureResult[] effects = new AdventureResult[ KoLConstants.activeEffects.size() ]; KoLConstants.activeEffects.toArray( effects ); for ( int i = 0; i < effects.length; ++i ) { String skillName = UneffectRequest.effectToSkill( effects[ i ].getName() ); if ( SkillDatabase.contains( skillName ) ) { int skillId = SkillDatabase.getSkillId( skillName ); if ( SkillDatabase.isAccordionThiefSong( skillId ) ) { count++; } } } return count; } private static final Boolean newSong( final int skillId ) { if ( !SkillDatabase.isAccordionThiefSong( skillId ) ) { return false; } AdventureResult[] effects = new AdventureResult[ KoLConstants.activeEffects.size() ]; KoLConstants.activeEffects.toArray( effects ); for ( int i = 0; i < effects.length; ++i ) { String skillName = UneffectRequest.effectToSkill( effects[ i ].getName() ); if ( SkillDatabase.contains( skillName ) ) { int effectSkillId = SkillDatabase.getSkillId( skillName ); if ( effectSkillId == skillId ) { return false; } } } return true; } @Override public void run() { if ( this.isRunning ) { return; } if ( GenericRequest.abortIfInFightOrChoice() ) { return; } UseSkillRequest.lastUpdate = ""; if ( this.buffCount == 0 ) { // Silently do nothing return; } if ( !KoLCharacter.hasSkill( this.skillName ) ) { UseSkillRequest.lastUpdate = "You don't know how to cast " + this.skillName + "."; return; } // Optimizing equipment can involve changing equipment. // Save a checkpoint so we can restore previous equipment. UseSkillRequest.optimizeEquipment( this.skillId ); if ( !KoLmafia.permitsContinue() ) { return; } int available = this.getMaximumCast(); if ( available == 0 ) { // We could print something return; } int desired = this.buffCount; if ( available < desired ) { // We SHOULD print something here KoLmafia.updateDisplay( "(Only " + available + " casts of " + this.skillName + " currently available.)" ); } this.setBuffCount( Math.min( desired, available ) ); if ( this.skillId == SkillPool.SUMMON_MINION || this.skillId == SkillPool.SUMMON_HORDE ) { ChoiceManager.setSkillUses( this.buffCount ); } this.isRunning = true; this.useSkillLoop(); this.isRunning = false; } private static final AdventureResult ONCE_CURSED = EffectPool.get( EffectPool.ONCE_CURSED ); private static final AdventureResult TWICE_CURSED = EffectPool.get( EffectPool.TWICE_CURSED ); private static final AdventureResult THRICE_CURSED = EffectPool.get( EffectPool.THRICE_CURSED ); private void useSkillLoop() { if ( KoLmafia.refusesContinue() ) { return; } int castsRemaining = this.buffCount; if ( castsRemaining == 0 ) { return; } if ( this.skillId == SkillPool.SHAKE_IT_OFF || ( this.skillId == SkillPool.BITE_MINION && KoLCharacter.hasSkill( "Devour Minions" ) ) ) { boolean cursed = KoLConstants.activeEffects.contains( UseSkillRequest.ONCE_CURSED ) || KoLConstants.activeEffects.contains( UseSkillRequest.TWICE_CURSED ) || KoLConstants.activeEffects.contains( UseSkillRequest.THRICE_CURSED ); // If on the Hidden Apartment Quest, and have a Curse, and skill will remove it, // ask if you are sure you want to lose it. if ( cursed && Preferences.getInteger( "hiddenApartmentProgress" ) < 7 && !InputFieldUtilities.confirm( "That will remove your Cursed effect. Are you sure?" ) ) { return; } } if ( this.skillId == SkillPool.RAINBOW_GRAVITATION ) { // Acquire necessary wads InventoryManager.retrieveItem( ItemPool.COLD_WAD, castsRemaining ); InventoryManager.retrieveItem( ItemPool.HOT_WAD, castsRemaining ); InventoryManager.retrieveItem( ItemPool.SLEAZE_WAD, castsRemaining ); InventoryManager.retrieveItem( ItemPool.SPOOKY_WAD, castsRemaining ); InventoryManager.retrieveItem( ItemPool.STENCH_WAD, castsRemaining ); InventoryManager.retrieveItem( ItemPool.TWINKLY_WAD, castsRemaining ); } int mpPerCast = SkillDatabase.getMPConsumptionById( this.skillId ); // If the skill doesn't use MP then MP restoring and checking can be skipped if ( mpPerCast == 0 ) { int soulsauceCost = SkillDatabase.getSoulsauceCost( this.skillId ); if ( soulsauceCost > 0 && KoLCharacter.getSoulsauce() < soulsauceCost ) { UseSkillRequest.lastUpdate = "Your available soulsauce is too low to cast " + this.skillName + "."; KoLmafia.updateDisplay( UseSkillRequest.lastUpdate ); return; } int thunderCost = SkillDatabase.getThunderCost( this.skillId ); if ( thunderCost > 0 && KoLCharacter.getThunder() < thunderCost ) { UseSkillRequest.lastUpdate = "You don't have enough thunder to cast " + this.skillName + "."; KoLmafia.updateDisplay( UseSkillRequest.lastUpdate ); return; } int rainCost = SkillDatabase.getRainCost( this.skillId ); if ( rainCost > 0 && KoLCharacter.getRain() < rainCost ) { UseSkillRequest.lastUpdate = "You have insufficient rain drops to cast " + this.skillName + "."; KoLmafia.updateDisplay( UseSkillRequest.lastUpdate ); return; } int lightningCost = SkillDatabase.getLightningCost( this.skillId ); if ( lightningCost > 0 && KoLCharacter.getLightning() < lightningCost ) { UseSkillRequest.lastUpdate = "You have too few lightning bolts to cast " + this.skillName + "."; KoLmafia.updateDisplay( UseSkillRequest.lastUpdate ); return; } boolean single = false; AdventureResult mana = SkillDatabase.getManaItemCost( this.skillId ); if ( mana != null ) { int manaPerCast = mana.getCount(); int manaNeeded = manaPerCast * castsRemaining; // getMaximumCast accounted for the "accessible // count" of the appropriate mana before we got // here. This should not fail. InventoryManager.retrieveItem( mana.getInstance( manaNeeded ) ); single = true; } if ( single ) { this.addFormField( this.countFieldId, "1" ); } else { this.addFormField( this.countFieldId, String.valueOf( castsRemaining ) ); castsRemaining = 1; } // Run it via GET String URLString = this.getFullURLString(); this.constructURLString( URLString, false ); while ( castsRemaining-- > 0 && !KoLmafia.refusesContinue() ) { super.run(); } // But keep fields as per POST for easy modification this.constructURLString( URLString, true ); return; } // Before executing the skill, recover all necessary mana int maximumMP = KoLCharacter.getMaximumMP(); int maximumCast = maximumMP / mpPerCast; // Save name so we can guarantee correct target later // *** Why, exactly, is this necessary? String originalTarget = this.target; // libram skills have variable (increasing) mana cost boolean isLibramSkill = SkillDatabase.isLibramSkill( this.skillId ); while ( castsRemaining > 0 && !KoLmafia.refusesContinue() ) { if ( isLibramSkill ) { mpPerCast = SkillDatabase.getMPConsumptionById( this.skillId ); } if ( maximumMP < mpPerCast ) { UseSkillRequest.lastUpdate = "Your maximum mana is too low to cast " + this.skillName + "."; KoLmafia.updateDisplay( UseSkillRequest.lastUpdate ); return; } // Find out how many times we can cast with current MP int currentCast = this.availableCasts( castsRemaining, mpPerCast ); // If none, attempt to recover MP in order to cast; // take auto-recovery into account. // Also recover MP if an opera mask is worn, to maximize its benefit. // (That applies only to AT buffs, but it's unlikely that an opera mask // will be worn at any other time than casting one.) boolean needExtra = currentCast < maximumCast && currentCast < castsRemaining && EquipmentManager.getEquipment( EquipmentManager.HAT ).getItemId() == ItemPool.OPERA_MASK; if ( currentCast == 0 || needExtra ) { currentCast = Math.min( castsRemaining, maximumCast ); int currentMP = KoLCharacter.getCurrentMP(); int recoverMP = mpPerCast * currentCast; if ( MoodManager.isExecuting() ) { recoverMP = Math.min( Math.max( recoverMP, MoodManager.getMaintenanceCost() ), maximumMP ); } SpecialOutfit.createImplicitCheckpoint(); RecoveryManager.recoverMP( recoverMP ); SpecialOutfit.restoreImplicitCheckpoint(); // If no change occurred, that means the person // was unable to recover MP; abort the process. if ( currentMP == KoLCharacter.getCurrentMP() ) { UseSkillRequest.lastUpdate = "Could not restore enough mana to cast " + this.skillName + "."; KoLmafia.updateDisplay( UseSkillRequest.lastUpdate ); return; } currentCast = this.availableCasts( castsRemaining, mpPerCast ); } if ( KoLmafia.refusesContinue() ) { UseSkillRequest.lastUpdate = "Error encountered during cast attempt."; return; } // If this happens to be a health-restorative skill, // then there is an effective cap based on how much // the skill is able to restore. switch ( this.skillId ) { case SkillPool.WALRUS_TONGUE: case SkillPool.DISCO_NAP: case SkillPool.BANDAGES: case SkillPool.COCOON: case SkillPool.SHAKE_IT_OFF: case SkillPool.GELATINOUS_RECONSTRUCTION: int healthRestored = HPRestoreItemList.getHealthRestored( this.skillName ); int maxPossible = Math.max( 1, ( KoLCharacter.getMaximumHP() - KoLCharacter.getCurrentHP() ) / healthRestored ); castsRemaining = Math.min( castsRemaining, maxPossible ); currentCast = Math.min( currentCast, castsRemaining ); break; } currentCast = Math.min( currentCast, maximumCast ); if ( currentCast > 0 ) { // Attempt to cast the buff. UseSkillRequest.optimizeEquipment( this.skillId ); if ( KoLmafia.refusesContinue() ) { UseSkillRequest.lastUpdate = "Error encountered during cast attempt."; return; } if ( this.isBuff ) { this.setTarget( originalTarget ); } if ( this.countFieldId != null ) { this.addFormField( this.countFieldId, String.valueOf( currentCast ), false ); } if ( this.target == null || this.target.trim().length() == 0 ) { KoLmafia.updateDisplay( "Casting " + this.skillName + " " + currentCast + " times..." ); } else { KoLmafia.updateDisplay( "Casting " + this.skillName + " on " + this.target + " " + currentCast + " times..." ); } // Run it via GET String URLString = this.getFullURLString(); this.constructURLString( URLString, false ); super.run(); // But keep fields as per POST for easy modification this.constructURLString( URLString, true ); // Otherwise, you have completed the correct // number of casts. Deduct it from the number // of casts remaining and continue. castsRemaining -= currentCast; } } if ( KoLmafia.refusesContinue() ) { UseSkillRequest.lastUpdate = "Error encountered during cast attempt."; } } public final int availableCasts( int maxCasts, int mpPerCast ) { int availableMP = KoLCharacter.getCurrentMP(); int currentCast = 0; if ( SkillDatabase.isLibramSkill( this.skillId ) ) { currentCast = SkillDatabase.libramSkillCasts( availableMP ); } else if ( SkillDatabase.isSoulsauceSkill( this.skillId ) ) { currentCast = KoLCharacter.getSoulsauce() / SkillDatabase.getSoulsauceCost( this.skillId ); } else if ( SkillDatabase.isThunderSkill( this.skillId ) ) { currentCast = KoLCharacter.getThunder() / SkillDatabase.getThunderCost( this.skillId ); } else if ( SkillDatabase.isRainSkill( this.skillId ) ) { currentCast = KoLCharacter.getRain() / SkillDatabase.getRainCost( this.skillId ); } else if ( SkillDatabase.isLightningSkill( this.skillId ) ) { currentCast = KoLCharacter.getLightning() / SkillDatabase.getLightningCost( this.skillId ); } else { currentCast = availableMP / mpPerCast; currentCast = Math.min( this.getMaximumCast(), currentCast ); } currentCast = Math.min( maxCasts, currentCast ); return currentCast; } private static final BuffTool findTool( BuffTool [] tools ) { for ( int i = 0; i < tools.length; ++i ) { BuffTool tool = tools[ i ]; if ( tool.hasItem( true ) ) { return tool; } } return null; } public static final boolean hasAccordion() { return KoLCharacter.canInteract() || UseSkillRequest.findTool( UseSkillRequest.THIEF_TOOLS ) != null; } public static final boolean hasTotem() { return KoLCharacter.canInteract() || UseSkillRequest.findTool( UseSkillRequest.TAMER_TOOLS ) != null; } public static final boolean hasSaucepan() { return KoLCharacter.canInteract() || UseSkillRequest.findTool( UseSkillRequest.SAUCE_TOOLS ) != null; } public static final void prepareTool( final BuffTool[] options, int skillId ) { if ( InventoryManager.canUseMall() || InventoryManager.canUseStorage() ) { // If we are here, you are out of Hardcore/Ronin and // have access to storage and the mall. // Iterate over tools. Retrieve the best one you have // available. If you have none available that are better // than the default tool, retrieve the default, which // is determined using these rules: // // 1) It is not a quest item and is therefore available // to any class. // 2) It is not expensive to buy // 3) It provides the most bonus turns of any tool that // satisfies the first two conditions for ( BuffTool tool : options ) { // If we have the tool, we are good to go if ( tool.hasItem( false ) && ( !tool.isClassLimited() || KoLCharacter.getClassType() == tool.getClassType() ) ) { // If it is not equipped, get it into inventory if ( !tool.hasEquipped() ) { tool.retrieveItem(); } return; } // If we don't have it and this is the default // tool on this list, acquire it. if ( tool.isDefault() ) { if ( !tool.retrieveItem() ) { KoLmafia.updateDisplay( MafiaState.ERROR, "You are out of Ronin and need a " + tool.getItem() + " to cast that. Check item retrieval settings." ); } return; } } } // If we are here, you are in Hardcore/Ronin and have access // only to what is in inventory (or closet, if your retrieval // settings allow you to use it). // Iterate over items and remember the best one you have available. BuffTool bestTool = null; for ( BuffTool tool : options ) { if ( tool.hasItem( false ) && ( !tool.isClassLimited() || ( KoLCharacter.getClassType() == tool.getClassType() ) ) ) { bestTool = tool; break; } } // If we don't have any of the tools, try to retrieve the // weakest one via purchase/sewer fishing. if ( bestTool == null ) { BuffTool weakestTool = options[ options.length - 1 ]; weakestTool.retrieveItem(); return; } // if best tool is equipped, cool. if ( bestTool.hasEquipped() ) { return; } // Get best tool into inventory bestTool.retrieveItem(); } @Override protected boolean retryOnTimeout() { return false; } @Override protected boolean processOnFailure() { return true; } @Override public void processResults() { UseSkillRequest.lastUpdate = ""; boolean shouldStop = UseSkillRequest.parseResponse( this.getURLString(), this.responseText ); if ( !UseSkillRequest.lastUpdate.equals( "" ) ) { MafiaState state = shouldStop ? MafiaState.ERROR : MafiaState.CONTINUE; KoLmafia.updateDisplay( state, UseSkillRequest.lastUpdate ); if ( BuffBotHome.isBuffBotActive() ) { BuffBotHome.timeStampedLogEntry( BuffBotHome.ERRORCOLOR, UseSkillRequest.lastUpdate ); } return; } if ( this.target == null ) { KoLmafia.updateDisplay( this.skillName + " was successfully cast." ); } else { KoLmafia.updateDisplay( this.skillName + " was successfully cast on " + this.target + "." ); } } @Override public boolean equals( final Object o ) { return o != null && o instanceof UseSkillRequest && this.getSkillName().equals( ( (UseSkillRequest) o ).getSkillName() ); } @Override public int hashCode() { return this.skillId; } public static final UseSkillRequest getUnmodifiedInstance( String skillName ) { if ( skillName == null || !SkillDatabase.contains( skillName ) ) { return null; } String canonical = StringUtilities.getCanonicalName( skillName ); UseSkillRequest request = (UseSkillRequest) UseSkillRequest.ALL_SKILLS.get( canonical ); if ( request == null ) { request = new UseSkillRequest( skillName ); UseSkillRequest.ALL_SKILLS.put( canonical, request ); } return request; } public static final UseSkillRequest getUnmodifiedInstance( final int skillId ) { return UseSkillRequest.getUnmodifiedInstance( SkillDatabase.getSkillName( skillId ) ); } public static final UseSkillRequest getInstance( final String skillName, final String target, final int buffCount ) { UseSkillRequest request = UseSkillRequest.getUnmodifiedInstance( skillName ); if ( request != null ) { request.setTarget( target == null || target.equals( "" ) ? KoLCharacter.getUserName() : target ); request.setBuffCount( buffCount ); } return request; } public static final UseSkillRequest getInstance( String skillName ) { return UseSkillRequest.getInstance( skillName, null, 0 ); } public static final UseSkillRequest getInstance( final int skillId ) { return UseSkillRequest.getInstance( SkillDatabase.getSkillName( skillId ) ); } public static final UseSkillRequest getInstance( final String skillName, final int buffCount ) { return UseSkillRequest.getInstance( skillName, null, buffCount ); } public static final UseSkillRequest getInstance( final String skillName, final Concoction conc ) { // Summon Clip Art UseSkillRequest request = UseSkillRequest.getUnmodifiedInstance( skillName ); if ( request != null ) { request.buffCount = 1; request.countFieldId = null; request.target = null; int param = conc.getParam(); int clip1 = ( param >> 16 ) & 0xFF; int clip2 = ( param >> 8 ) & 0xFF; int clip3 = ( param ) & 0xFF; request.addFormField( "clip1", String.valueOf( clip1 ) ); request.addFormField( "clip2", String.valueOf( clip2 ) ); request.addFormField( "clip3", String.valueOf( clip3 ) ); } return request; } public static final boolean parseResponse( final String urlString, final String responseText ) { int skillId = UseSkillRequest.lastSkillUsed; int count = UseSkillRequest.lastSkillCount; if ( urlString.contains( "skillz.php" ) && !urlString.contains( "whichskill" ) ) { // This is a skill list, parse skills for consequences. Matcher matcher = UseSkillRequest.SKILLZ_PATTERN.matcher( responseText ); while ( matcher.find() ) { ConsequenceManager.parseSkillDesc( StringUtilities.parseInt( matcher.group( 1 ) ), matcher.group( 2 ) ); } } if ( skillId == -1 ) { UseSkillRequest.lastUpdate = "Skill ID not saved."; return false; } UseSkillRequest.lastSkillUsed = -1; UseSkillRequest.lastSkillCount = 0; if ( responseText == null || responseText.trim().length() == 0 ) { int initialMP = KoLCharacter.getCurrentMP(); ApiRequest.updateStatus(); if ( initialMP == KoLCharacter.getCurrentMP() ) { UseSkillRequest.lastUpdate = "Encountered lag problems."; return false; } UseSkillRequest.lastUpdate = "KoL sent back a blank response, but consumed MP."; return true; } if ( responseText.contains( "You don't have that skill" ) ) { UseSkillRequest.lastUpdate = "That skill is unavailable."; return true; } if ( responseText.contains( "You may only use three Tome summonings each day" ) ) { Preferences.setInteger( "tomeSummons", 3 ); if ( KoLCharacter.canInteract() ) { switch ( skillId ) { case SkillPool.SNOWCONE: Preferences.setInteger( "_snowconeSummons", 3 ); break; case SkillPool.STICKER: Preferences.setInteger( "_stickerSummons", 3 ); break; case SkillPool.SUGAR: Preferences.setInteger( "_sugarSummons", 3 ); break; case SkillPool.RAD_LIB: Preferences.setInteger( "_radlibSummons", 3 ); break; case SkillPool.SMITHSNESS: Preferences.setInteger( "_smithsnessSummons", 3 ); break; } } else { UseSkillRequest.lastUpdate = "You've used your Tomes enough today."; } ConcoctionDatabase.setRefreshNeeded( true ); return true; } // Summon Clip Art cast through the browser has two phases: // // campground.php?preaction=summoncliparts // campground.php?preaction=combinecliparts // // Only the second once consumes MP and only if it is successful. // Internally, we use only the second URL. // // For now, simply ignore any call on either URL that doesn't // result in an item, since failures just redisplay the bookshelf if ( skillId == SkillPool.CLIP_ART && !responseText.contains( "You acquire" ) ) { return false; } // You can't fit anymore songs in your head right now // XXX can't fit anymore songs in their head right now. if ( responseText.contains( "can't fit anymore songs" ) || responseText.contains( "can't fit any more songs" ) ) { UseSkillRequest.lastUpdate = "Selected target has the maximum number of AT buffs already."; return false; } if ( responseText.contains( "casts left of the Smile of Mr. A" ) ) { UseSkillRequest.lastUpdate = "You cannot cast that many smiles."; return false; } if ( responseText.contains( "Invalid target player" ) ) { UseSkillRequest.lastUpdate = "Selected target is not a valid target."; return true; } // You can't cast that spell on persons who are lower than // level 15, like <name>, who is level 13. if ( responseText.contains( "lower than level" ) ) { UseSkillRequest.lastUpdate = "Selected target is too low level."; return false; } if ( responseText.contains( "busy fighting" ) ) { UseSkillRequest.lastUpdate = "Selected target is busy fighting."; return false; } if ( responseText.contains( "receive buffs" ) ) { UseSkillRequest.lastUpdate = "Selected target cannot receive buffs."; return false; } if ( responseText.contains( "You need" ) ) { UseSkillRequest.lastUpdate = "You need special equipment to cast that buff."; return true; } if ( responseText.contains( "You can't remember how to use that skill" ) ) { UseSkillRequest.lastUpdate = "That skill is currently unavailable."; return true; } if ( responseText.contains( "You can't cast this spell because you are not an Accordion Thief" ) ) { UseSkillRequest.lastUpdate = "Only Accordion Thieves can use that skill."; return true; } if ( responseText.contains( "You're already blessed" ) ) { UseSkillRequest.lastUpdate = "You already have that blessing."; return true; } if ( responseText.contains( "not attuned to any particular Turtle Spirit" ) ) { UseSkillRequest.lastUpdate = "You haven't got a Blessing, so can't get a Boon."; return true; } if ( responseText.contains( "You can only declare one Employee of the Month per day" ) ) { UseSkillRequest.lastUpdate = "You can only declare one Employee of the Month per day."; Preferences.setBoolean( "_managerialManipulationUsed", true ); return true; } // You've already recalled a lot of ancestral memories lately. You should // probably give your ancestors the rest of the day off. if ( responseText.contains( "You've already recalled a lot of ancestral memories lately" ) ) { UseSkillRequest.lastUpdate = "You can only cast Ancestral Recall 10 times per day."; Preferences.setInteger( "_ancestralRecallCasts", 10 ); return true; } // You think your stomach has had enough for one day. if ( responseText.contains( "enough for one day" ) ) { UseSkillRequest.lastUpdate = "You can only do that once a day."; Preferences.setBoolean( "_carboLoaded", true ); return false; } // You can't cast that many turns of that skill today. (You've used 5 casts today, // and the limit of casts per day you have is 5.) if ( responseText.contains( "You can't cast that many turns of that skill today" ) ) { UseSkillRequest.lastUpdate = "You've reached your daily casting limit for that skill."; switch ( skillId ) { case SkillPool.THINGFINDER: Preferences.setInteger( "_thingfinderCasts", 10 ); break; case SkillPool.BENETTONS: Preferences.setInteger( "_benettonsCasts", 10 ); break; case SkillPool.ELRONS: Preferences.setInteger( "_elronsCasts", 10 ); break; case SkillPool.COMPANIONSHIP: Preferences.setInteger( "_companionshipCasts", 10 ); break; case SkillPool.PRECISION: Preferences.setInteger( "_precisionCasts", 10 ); break; case SkillPool.DONHOS: Preferences.setInteger( "_donhosCasts", 50 ); break; case SkillPool.INIGOS: Preferences.setInteger( "_inigosCasts", 5 ); break; default: break; } return false; } Matcher limitedMatcher = UseSkillRequest.LIMITED_PATTERN.matcher( responseText ); // limited-use skills // "Y / maxCasts casts used today." if ( limitedMatcher.find() ) { int casts = 0; // parse the number of casts remaining and set the appropriate preference. String numString = limitedMatcher.group( 1 ); casts = Integer.parseInt( numString ); switch ( skillId ) { case SkillPool.THINGFINDER: Preferences.setInteger( "_thingfinderCasts", casts ); break; case SkillPool.BENETTONS: Preferences.setInteger( "_benettonsCasts", casts ); break; case SkillPool.ELRONS: Preferences.setInteger( "_elronsCasts", casts ); break; case SkillPool.COMPANIONSHIP: Preferences.setInteger( "_companionshipCasts", casts ); break; case SkillPool.PRECISION: Preferences.setInteger( "_precisionCasts", casts ); break; case SkillPool.DONHOS: Preferences.setInteger( "_donhosCasts", casts ); break; case SkillPool.INIGOS: Preferences.setInteger( "_inigosCasts", casts ); break; } } if ( responseText.contains( "You don't have enough" ) ) { String skillName = SkillDatabase.getSkillName( skillId ); UseSkillRequest.lastUpdate = "Not enough mana to cast " + skillName + "."; ApiRequest.updateStatus(); return true; } // The skill was successfully cast. Deal with its effects. if ( responseText.contains( "tear the opera mask" ) ) { EquipmentManager.breakEquipment( ItemPool.OPERA_MASK, "Your opera mask shattered." ); } int mpCost = SkillDatabase.getMPConsumptionById( skillId ) * count; if ( responseText.contains( "You can only conjure" ) || responseText.contains( "You can only scrounge up" ) || responseText.contains( "You can't use that skill" ) || responseText.contains( "You can only summon" ) ) { if ( skillId == SkillPool.COCOON ) { // Cannelloni Cocoon says "You can't use that // skill" when you are already at full HP. UseSkillRequest.lastUpdate = "You are already at full HP."; } else if ( skillId == SkillPool.ANCESTRAL_RECALL ) { // Ancestral Recall says "You can't use that // skill" if you don't have any blue mana. UseSkillRequest.lastUpdate = "You don't have any blue mana."; return true; } else { UseSkillRequest.lastUpdate = "Summon limit exceeded."; // We're out of sync with the actual number of times // this skill has been cast. Adjust the counter by 1 // at a time. count = 1; } mpCost = 0; } switch ( skillId ) { case SkillPool.ODE_TO_BOOZE: ConcoctionDatabase.getUsables().sort(); break; case SkillPool.WALRUS_TONGUE: case SkillPool.DISCO_NAP: UneffectRequest.removeEffectsWithSkill( skillId ); break; case SkillPool.SMILE_OF_MR_A: Preferences.increment( "_smilesOfMrA", count ); break; case SkillPool.RAGE_GLAND: Preferences.setBoolean( "rageGlandVented", true ); break; case SkillPool.RAINBOW_GRAVITATION: // Each cast of Rainbow Gravitation consumes five // elemental wads and a twinkly wad ResultProcessor.processResult( ItemPool.get( ItemPool.COLD_WAD, -count ) ); ResultProcessor.processResult( ItemPool.get( ItemPool.HOT_WAD, -count ) ); ResultProcessor.processResult( ItemPool.get( ItemPool.SLEAZE_WAD, -count ) ); ResultProcessor.processResult( ItemPool.get( ItemPool.SPOOKY_WAD, -count ) ); ResultProcessor.processResult( ItemPool.get( ItemPool.STENCH_WAD, -count ) ); ResultProcessor.processResult( ItemPool.get( ItemPool.TWINKLY_WAD, -count ) ); Preferences.increment( "prismaticSummons", count ); break; case SkillPool.LUNCH_BREAK: Preferences.setBoolean( "_lunchBreak", true ); break; case SkillPool.SPAGHETTI_BREAKFAST: Preferences.setBoolean( "_spaghettiBreakfast", true ); break; case SkillPool.GRAB_A_COLD_ONE: Preferences.setBoolean( "_coldOne", true ); break; case SkillPool.THATS_NOT_A_KNIFE: Preferences.setBoolean( "_discoKnife", true ); break; case SkillPool.TURTLE_POWER: Preferences.setBoolean( "_turtlePowerCast", true ); Preferences.setInteger( "turtleBlessingTurns", 0 ); break; case SkillPool.WAR_BLESSING: case SkillPool.SHE_WHO_WAS_BLESSING: case SkillPool.STORM_BLESSING: Preferences.setInteger( "turtleBlessingTurns", 0 ); break; case SkillPool.SUMMON_BONERS: Preferences.setBoolean( "_bonersSummoned", true ); break; case SkillPool.REQUEST_SANDWICH: // You take a deep breath and prepare for a Boris-style bellow. Then you remember your manners // and shout, "If it's not too much trouble, I'd really like a sandwich right now! Please!" // To your surprise, it works! Someone wanders by slowly and hands you a sandwich, grumbling, // "well, since you asked nicely . . ." if ( responseText.contains( "well, since you asked nicely" ) ) { Preferences.setBoolean( "_requestSandwichSucceeded", true ); } break; case SkillPool.PASTAMASTERY: Preferences.increment( "noodleSummons", count ); break; case SkillPool.CARBOLOADING: Preferences.setBoolean( "_carboLoaded", true ); Preferences.increment( "carboLoading", 1 ); break; case SkillPool.ADVANCED_SAUCECRAFTING: Preferences.increment( "reagentSummons", count ); break; case SkillPool.ADVANCED_COCKTAIL: Preferences.increment( "cocktailSummons", count ); break; case SkillPool.DEMAND_SANDWICH: Preferences.increment( "_demandSandwich", count ); break; case SkillPool.SNOWCONE: Preferences.increment( "_snowconeSummons", count ); Preferences.increment( "tomeSummons", count ); ConcoctionDatabase.setRefreshNeeded( false ); break; case SkillPool.STICKER: Preferences.increment( "_stickerSummons", count ); Preferences.increment( "tomeSummons", count ); ConcoctionDatabase.setRefreshNeeded( false ); break; case SkillPool.SUGAR: Preferences.increment( "_sugarSummons", count ); Preferences.increment( "tomeSummons", count ); ConcoctionDatabase.setRefreshNeeded( false ); break; case SkillPool.CLIP_ART: Preferences.increment( "_clipartSummons", count ); Preferences.increment( "tomeSummons", count ); ConcoctionDatabase.setRefreshNeeded( false ); break; case SkillPool.RAD_LIB: Preferences.increment( "_radlibSummons", count ); Preferences.increment( "tomeSummons", count ); ConcoctionDatabase.setRefreshNeeded( false ); break; case SkillPool.SMITHSNESS: Preferences.increment( "_smithsnessSummons", count ); Preferences.increment( "tomeSummons", count ); ConcoctionDatabase.setRefreshNeeded( false ); break; case SkillPool.HILARIOUS: Preferences.increment( "grimoire1Summons", 1 ); break; case SkillPool.TASTEFUL: Preferences.increment( "grimoire2Summons", 1 ); break; case SkillPool.CARDS: Preferences.increment( "grimoire3Summons", 1 ); break; case SkillPool.GEEKY: Preferences.increment( "_grimoireGeekySummons", 1 ); break; case SkillPool.CONFISCATOR: Preferences.increment( "_grimoireConfiscatorSummons", 1 ); break; case SkillPool.CRIMBO_CANDY: Preferences.increment( "_candySummons", 1 ); break; case SkillPool.PSYCHOKINETIC_HUG: Preferences.setBoolean( "_psychokineticHugUsed", true ); break; case SkillPool.MANAGERIAL_MANIPULATION: Preferences.setBoolean( "_managerialManipulationUsed", true ); break; case SkillPool.CONJURE_EGGS: Preferences.setBoolean( "_jarlsEggsSummoned", true ); break; case SkillPool.CONJURE_DOUGH: Preferences.setBoolean( "_jarlsDoughSummoned", true ); break; case SkillPool.CONJURE_VEGGIES: Preferences.setBoolean( "_jarlsVeggiesSummoned", true ); break; case SkillPool.CONJURE_CHEESE: Preferences.setBoolean( "_jarlsCheeseSummoned", true ); break; case SkillPool.CONJURE_MEAT: Preferences.setBoolean( "_jarlsMeatSummoned", true ); break; case SkillPool.CONJURE_POTATO: Preferences.setBoolean( "_jarlsPotatoSummoned", true ); break; case SkillPool.CONJURE_CREAM: Preferences.setBoolean( "_jarlsCreamSummoned", true ); break; case SkillPool.CONJURE_FRUIT: Preferences.setBoolean( "_jarlsFruitSummoned", true ); break; case SkillPool.EGGMAN: ResultProcessor.removeItem( ItemPool.COSMIC_EGG ); break; case SkillPool.RADISH_HORSE: ResultProcessor.removeItem( ItemPool.COSMIC_VEGETABLE ); break; case SkillPool.HIPPOTATO: ResultProcessor.removeItem( ItemPool.COSMIC_POTATO ); break; case SkillPool.CREAMPUFF: ResultProcessor.removeItem( ItemPool.COSMIC_CREAM ); break; case SkillPool.DEEP_VISIONS: DreadScrollManager.handleDeepDarkVisions( responseText ); break; case SkillPool.BIND_VAMPIEROGHI: case SkillPool.BIND_VERMINCELLI: case SkillPool.BIND_ANGEL_HAIR_WISP: case SkillPool.BIND_UNDEAD_ELBOW_MACARONI: case SkillPool.BIND_PENNE_DREADFUL: case SkillPool.BIND_LASAGMBIE: case SkillPool.BIND_SPICE_GHOST: case SkillPool.BIND_SPAGHETTI_ELEMENTAL: PastaThrallData.handleBinding( skillId, responseText ); break; case SkillPool.DISMISS_PASTA_THRALL: PastaThrallData.handleDismissal( responseText ); break; case SkillPool.THROW_PARTY: Preferences.setBoolean( "_petePartyThrown", true ); break; case SkillPool.INCITE_RIOT: Preferences.setBoolean( "_peteRiotIncited", true ); break; case SkillPool.SUMMON_ANNOYANCE: Preferences.setBoolean( "_summonAnnoyanceUsed", true ); Preferences.decrement( "availableSwagger", Preferences.getInteger( "summonAnnoyanceCost" ) ); break; case SkillPool.PIRATE_BELLOW: Preferences.setBoolean( "_pirateBellowUsed", true ); break; case SkillPool.HOLIDAY_FUN: Preferences.setBoolean( "_holidayFunUsed", true ); break; case SkillPool.SUMMON_CARROT: Preferences.setBoolean( "_summonCarrotUsed", true ); break; case SkillPool.SUMMON_KOKOMO_RESORT_PASS: Preferences.setBoolean( "_summonResortPassUsed", true ); break; case SkillPool.ANCESTRAL_RECALL: Preferences.increment( "_ancestralRecallCasts", count ); ResultProcessor.processResult( ItemPool.get( ItemPool.BLUE_MANA, -count ) ); break; case SkillPool.DARK_RITUAL: ResultProcessor.processResult( ItemPool.get( ItemPool.BLACK_MANA, -count ) ); break; case SkillPool.PERFECT_FREEZE: Preferences.setBoolean( "_perfectFreezeUsed", true ); break; case SkillPool.COMMUNISM: Preferences.setBoolean( "_communismUsed", true ); break; case SkillPool.BOW_LEGGED_SWAGGER: Preferences.setBoolean( "_bowleggedSwaggerUsed", true ); break; case SkillPool.BEND_HELL: Preferences.setBoolean( "_bendHellUsed", true ); break; case SkillPool.STEELY_EYED_SQUINT: Preferences.setBoolean( "_steelyEyedSquintUsed", true ); break; case SkillPool.CECI_CHAPEAU: Preferences.setBoolean( "_ceciHatUsed", true ); break; case SkillPool.STACK_LUMPS: Preferences.increment( "_stackLumpsUses" ); ResultProcessor.processResult( ItemPool.get( ItemPool.NEGATIVE_LUMP, -100 ) ); break; case SkillPool.EVOKE_ELDRITCH_HORROR: Preferences.setBoolean( "_eldritchHorrorEvoked", true ); break; } if ( SkillDatabase.isLibramSkill( skillId ) ) { int cast = Preferences.getInteger( "libramSummons" ); mpCost = SkillDatabase.libramSkillMPConsumption( cast + 1, count ); Preferences.increment( "libramSummons", count ); LockableListFactory.sort( KoLConstants.summoningSkills ); LockableListFactory.sort( KoLConstants.usableSkills ); } else if ( SkillDatabase.isSoulsauceSkill( skillId ) ) { KoLCharacter.decrementSoulsauce( SkillDatabase.getSoulsauceCost( skillId ) * count ); } else if ( SkillDatabase.isThunderSkill( skillId ) ) { KoLCharacter.decrementThunder( SkillDatabase.getThunderCost( skillId ) * count ); } else if ( SkillDatabase.isRainSkill( skillId ) ) { KoLCharacter.decrementRain( SkillDatabase.getRainCost( skillId ) * count ); } else if ( SkillDatabase.isLightningSkill( skillId ) ) { KoLCharacter.decrementLightning( SkillDatabase.getLightningCost( skillId ) * count ); } if ( mpCost > 0 ) { ResultProcessor.processResult( new AdventureResult( AdventureResult.MP, 0 - mpCost ) ); } return false; } public static int getSkillId( final String urlString ) { Matcher skillMatcher = UseSkillRequest.SKILLID_PATTERN.matcher( urlString ); if ( skillMatcher.find() ) { return StringUtilities.parseInt( skillMatcher.group( 1 ) ); } skillMatcher = UseSkillRequest.BOOKID_PATTERN.matcher( urlString ); if ( !skillMatcher.find() ) { return -1; } String action = skillMatcher.group( 1 ); if ( action.equals( "snowcone" ) ) { return SkillPool.SNOWCONE; } if ( action.equals( "stickers" ) ) { return SkillPool.STICKER; } if ( action.equals( "sugarsheets" ) ) { return SkillPool.SUGAR; } if ( action.equals( "cliparts" ) ) { if ( !urlString.contains( "clip3=" ) ) { return -1; } return SkillPool.CLIP_ART; } if ( action.equals( "radlibs" ) ) { return SkillPool.RAD_LIB; } if ( action.equals( "smithsness" ) ) { return SkillPool.SMITHSNESS; } if ( action.equals( "hilariousitems" ) ) { return SkillPool.HILARIOUS; } if ( action.equals( "spencersitems" ) ) { return SkillPool.TASTEFUL; } if ( action.equals( "aa" ) ) { return SkillPool.CARDS; } if ( action.equals( "thinknerd" ) ) { return SkillPool.GEEKY; } if ( action.equals( "candyheart" ) ) { return SkillPool.CANDY_HEART; } if ( action.equals( "partyfavor" ) ) { return SkillPool.PARTY_FAVOR; } if ( action.equals( "lovesongs" ) ) { return SkillPool.LOVE_SONG; } if ( action.equals( "brickos" ) ) { return SkillPool.BRICKOS; } if ( action.equals( "gygax" ) ) { return SkillPool.DICE; } if ( action.equals( "resolutions" ) ) { return SkillPool.RESOLUTIONS; } if ( action.equals( "taffy" ) ) { return SkillPool.TAFFY; } if ( action.equals( "confiscators" ) ) { return SkillPool.CONFISCATOR; } return -1; } private static final int getCount( final String urlString, int skillId ) { Matcher countMatcher = UseSkillRequest.COUNT_PATTERN.matcher( urlString ); if ( !countMatcher.find() ) { return 1; } int availableMP = KoLCharacter.getCurrentMP(); int maxcasts; if ( SkillDatabase.isLibramSkill( skillId ) ) { maxcasts = SkillDatabase.libramSkillCasts( availableMP ); } else if ( SkillDatabase.isSoulsauceSkill( skillId ) ) { maxcasts = KoLCharacter.getSoulsauce() / SkillDatabase.getSoulsauceCost( skillId ); } else if ( SkillDatabase.isThunderSkill( skillId ) ) { maxcasts = KoLCharacter.getThunder() / SkillDatabase.getThunderCost( skillId ); } else if ( SkillDatabase.isRainSkill( skillId ) ) { maxcasts = KoLCharacter.getRain() / SkillDatabase.getRainCost( skillId ); } else if ( SkillDatabase.isLightningSkill( skillId ) ) { maxcasts = KoLCharacter.getLightning() / SkillDatabase.getLightningCost( skillId ); } else { int MP = SkillDatabase.getMPConsumptionById( skillId ); maxcasts = SkillDatabase.getMaxCasts( skillId ); maxcasts = maxcasts == -1 ? Integer.MAX_VALUE : maxcasts; if ( MP != 0 ) { maxcasts = Math.min( maxcasts, availableMP / MP ); } } if ( countMatcher.group( 1 ).startsWith( "*" ) ) { return maxcasts; } return Math.min( maxcasts, StringUtilities.parseInt( countMatcher.group( 1 ) ) ); } public static final boolean registerRequest( final String urlString ) { if ( urlString.startsWith( "skillz.php" ) ) { return true; } if ( !urlString.startsWith( "campground.php" ) && !urlString.startsWith( "runskillz.php" ) ) { return false; } int skillId = UseSkillRequest.getSkillId( urlString ); // Quick skills has (select a skill) with ID = 999 if ( skillId == -1 || skillId == 999 ) { return false; } int count = UseSkillRequest.getCount( urlString, skillId ); String skillName = SkillDatabase.getSkillName( skillId ); UseSkillRequest.lastSkillUsed = skillId; UseSkillRequest.lastSkillCount = count; RequestLogger.updateSessionLog(); RequestLogger.updateSessionLog( "cast " + count + " " + skillName ); SkillDatabase.registerCasts( skillId, count ); return true; } @Override public int getAdventuresUsed() { return SkillDatabase.getAdventureCost( this.skillId ); } public static int getAdventuresUsed( final String urlString ) { return SkillDatabase.getAdventureCost( UseSkillRequest.getSkillId( urlString ) ); } public static class BuffTool { final AdventureResult item; final int bonusTurns; final boolean def; final String classType; public BuffTool( final int itemId, final int bonusTurns, final boolean def, final String classType ) { this.item = ItemPool.get( itemId, 1 ); this.bonusTurns = bonusTurns; this.def = def; this.classType = classType; } public final AdventureResult getItem() { return this.item; } public final int getBonusTurns() { return this.bonusTurns; } public final boolean isClassLimited() { return this.classType != null; } public final String getClassType() { return this.classType; } public final boolean isDefault() { return this.def; } public final boolean hasEquipped() { return KoLCharacter.hasEquipped( this.item ); } public final boolean hasItem( final boolean create ) { return InventoryManager.hasItem( this.item, create ); } public final boolean retrieveItem() { return InventoryManager.retrieveItem( this.item ); } } }