/** * 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; import java.lang.CloneNotSupportedException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.StringTokenizer; import net.sourceforge.kolmafia.objectpool.ItemPool; import net.sourceforge.kolmafia.persistence.BountyDatabase; import net.sourceforge.kolmafia.persistence.ConsumablesDatabase; import net.sourceforge.kolmafia.persistence.EffectDatabase; import net.sourceforge.kolmafia.persistence.ItemDatabase; import net.sourceforge.kolmafia.persistence.ItemFinder; import net.sourceforge.kolmafia.persistence.SkillDatabase; import net.sourceforge.kolmafia.preferences.Preferences; import net.sourceforge.kolmafia.request.PurchaseRequest; import net.sourceforge.kolmafia.request.UneffectRequest; import net.sourceforge.kolmafia.session.GoalManager; import net.sourceforge.kolmafia.session.InventoryManager; import net.sourceforge.kolmafia.utilities.StringUtilities; public class AdventureResult implements Comparable<AdventureResult>, Cloneable { public static final String[] STAT_NAMES = { "muscle", "mysticality", "moxie" }; protected int priority; protected int id; protected String name; private int count; private static final int NO_PRIORITY = 0; private static final int ADV_PRIORITY = 1; private static final int MEAT_PRIORITY = 2; protected static final int SUBSTAT_PRIORITY = 3; protected static final int FULLSTAT_PRIORITY = 4; private static final int ITEM_PRIORITY = 5; private static final int EFFECT_PRIORITY = 6; private static final int BOUNTY_ITEM_PRIORITY = 6; public static final int PSEUDO_ITEM_PRIORITY = 99; protected static final int MONSTER_PRIORITY = -1; public static final String HP = "HP"; public static final String MP = "MP"; public static final String ADV = "Adv"; public static final String ARENA_ML = "Arena flyer ML"; public static final String AUTOSTOP = "Autostop"; public static final String CHASM_BRIDGE = "Chasm Bridge Progress"; public static final String CHOICE = "Choice"; public static final String DRUNK = "Drunk"; public static final String EXTRUDE = "Source Terminal Extrude"; public static final String FACTOID = "Factoid"; public static final String FLOUNDRY = "Floundry Fish"; public static final String FREE_CRAFT = "Free Craft"; public static final String FULL = "Fullness"; public static final String MEAT = "Meat"; public static final String MEAT_SPENT = "Meat Spent"; public static final String PIRATE_INSULT = "pirate insult"; public static final String PULL = "Pull"; public static final String PVP = "PvP"; public static final String STILL = "Still"; public static final String TOME = "Tome Summon"; // Sub/full stats have multiple values and should be delegated // to AdventureMultiResult. public static final String SUBSTATS = "Substats"; public static final String FULLSTATS = "Fullstats"; public static final List<String> MUS_SUBSTAT = new ArrayList<String>(); public static final List<String> MYS_SUBSTAT = new ArrayList<String>(); public static final List<String> MOX_SUBSTAT = new ArrayList<String>(); static { AdventureResult.MUS_SUBSTAT.add( "Beefiness" ); AdventureResult.MUS_SUBSTAT.add( "Fortitude" ); AdventureResult.MUS_SUBSTAT.add( "Muscleboundness" ); AdventureResult.MUS_SUBSTAT.add( "Strengthliness" ); AdventureResult.MUS_SUBSTAT.add( "Strongness" ); // The following only under Can Has Cyborger AdventureResult.MUS_SUBSTAT.add( "muskewlairtees" ); AdventureResult.MYS_SUBSTAT.add( "Enchantedness" ); AdventureResult.MYS_SUBSTAT.add( "Magicalness" ); AdventureResult.MYS_SUBSTAT.add( "Mysteriousness" ); AdventureResult.MYS_SUBSTAT.add( "Wizardliness" ); // The following only under Can Has Cyborger AdventureResult.MYS_SUBSTAT.add( "mistikkaltees" ); AdventureResult.MOX_SUBSTAT.add( "Cheek" ); AdventureResult.MOX_SUBSTAT.add( "Chutzpah" ); AdventureResult.MOX_SUBSTAT.add( "Roguishness" ); AdventureResult.MOX_SUBSTAT.add( "Sarcasm" ); AdventureResult.MOX_SUBSTAT.add( "Smarm" ); // The following only under Can Has Cyborger AdventureResult.MOX_SUBSTAT.add( "mawksees" ); } public static final int[] SESSION_SUBSTATS = new int[ 3 ]; public static final AdventureResult SESSION_SUBSTATS_RESULT = new AdventureMultiResult( AdventureResult.SUBSTATS, AdventureResult.SESSION_SUBSTATS ); public static final int[] SESSION_FULLSTATS = new int[ 3 ]; public static final AdventureResult SESSION_FULLSTATS_RESULT = new AdventureMultiResult( AdventureResult.FULLSTATS, AdventureResult.SESSION_FULLSTATS ); /** * Constructs a new <code>AdventureResult</code> with the given name. The amount of gain will default to zero. * * @param name The name of the result */ public AdventureResult( final String name ) { this( AdventureResult.choosePriority( name ), name, 0 ); } public AdventureResult( final String name, final int count ) { this( AdventureResult.choosePriority( name ), name, count ); } public AdventureResult( final String name, final int count, final boolean isStatusEffect ) { this( isStatusEffect ? EFFECT_PRIORITY : ITEM_PRIORITY, name, count ); } public AdventureResult( final int subType, final String name ) { this( subType, name, 1 ); } public AdventureResult( final int subType, final String name, final int count ) { this.name = name; this.count = count; this.priority = subType; this.id = -1; if ( this.priority == AdventureResult.EFFECT_PRIORITY ) { // This will also set this.id as appropriate this.normalizeEffectName(); } else if ( this.priority == AdventureResult.ITEM_PRIORITY ) { // This will also set this.id as appropriate this.normalizeItemName(); } else if ( this.priority == AdventureResult.PSEUDO_ITEM_PRIORITY ) { // Detach substring from larger text this.name = new String( name ); this.priority = AdventureResult.ITEM_PRIORITY; } else { // Detach substring from larger text this.name = new String( name ); } } protected static int choosePriority( final String name ) { if ( name.equals( AdventureResult.ADV ) || name.equals( AdventureResult.CHOICE ) || name.equals( AdventureResult.AUTOSTOP ) || name.equals( AdventureResult.FACTOID ) || name.equals( AdventureResult.PULL ) || name.equals( AdventureResult.STILL ) || name.equals( AdventureResult.TOME )|| name.equals( AdventureResult.EXTRUDE )|| name.equals( AdventureResult.FREE_CRAFT ) ) { return AdventureResult.ADV_PRIORITY; } if ( name.equals( AdventureResult.MEAT ) || name.equals( AdventureResult.MEAT_SPENT ) ) { return AdventureResult.MEAT_PRIORITY; } if ( name.equals( AdventureResult.HP ) || name.equals( AdventureResult.MP ) || name.equals( AdventureResult.DRUNK ) || name.equals( AdventureResult.FULL ) || name.equals( AdventureResult.PVP ) ) { return AdventureResult.NO_PRIORITY; } if ( name.equals( AdventureResult.SUBSTATS ) ) { return AdventureResult.SUBSTAT_PRIORITY; } if ( name.equals( AdventureResult.FULLSTATS ) ) { return AdventureResult.FULLSTAT_PRIORITY; } if ( name.equals( AdventureResult.FLOUNDRY ) ) { return AdventureResult.PSEUDO_ITEM_PRIORITY; } if ( BountyDatabase.getType( name ) != null ) { return AdventureResult.BOUNTY_ITEM_PRIORITY; } if ( EffectDatabase.contains( name ) ) { return AdventureResult.EFFECT_PRIORITY; } return AdventureResult.ITEM_PRIORITY; } public AdventureResult( final int id, final int count, final boolean isStatusEffect ) { if ( isStatusEffect ) { String name = EffectDatabase.getEffectName( id ); this.name = name != null ? name : "(unknown effect " + String.valueOf( id ) + ")"; this.priority = AdventureResult.EFFECT_PRIORITY; } else { String name = ItemDatabase.getItemDataName( id ); this.name = name != null ? name : "(unknown item " + String.valueOf( id ) + ")"; this.priority = AdventureResult.ITEM_PRIORITY; } this.id = id; this.count = count; } public AdventureResult( final String name, final int id, final int count, final boolean isStatusEffect ) { this.name = name; this.id = id; this.count = count; this.priority = isStatusEffect ? AdventureResult.EFFECT_PRIORITY : AdventureResult.ITEM_PRIORITY; } // Need this to retain instance-specific methods protected Object clone() throws CloneNotSupportedException { return super.clone(); } public void normalizeEffectName() { this.priority = AdventureResult.EFFECT_PRIORITY; if ( this.name == null ) { this.name = "(unknown effect)"; return; } this.id = EffectDatabase.getEffectId( this.name ); if ( this.id != -1 ) { String name = EffectDatabase.getEffectName( this.id ); if ( name != null ) { this.name = name; } else { RequestLogger.printLine( "Effect database error: id = " + this.id + " name = \"" + this.name + "\"" ); } } else { this.name = new String( this.name ); } } public void normalizeItemName() { this.priority = AdventureResult.ITEM_PRIORITY; if ( this.name == null ) { this.name = "(unknown item " + String.valueOf( this.id ) + ")"; return; } if ( this.name.equals( "(none)" ) || this.name.equals( "-select an item-" ) ) { return; } this.id = ItemDatabase.getItemId( this.name, this.getCount() ); if ( this.id > 0 ) { String name = ItemDatabase.getItemDataName( this.id ); if ( name != null ) { this.name = name; } else { RequestLogger.printLine( "Item database error: id = " + this.id + " name = \"" + this.name + "\"" ); } } else { this.name = new String( this.name ); RequestLogger.printLine( "Unknown item found: " + this.name ); } } public static final AdventureResult pseudoItem( final String name ) { AdventureResult item = ItemFinder.getFirstMatchingItem( name, false ); if ( item != null ) { return item; } // Make a pseudo-item with the required name return new AdventureResult( name, -1, 1 , false ); } public static final AdventureResult tallyItem( final String name ) { return AdventureResult.tallyItem( name, true ); } public static final AdventureResult tallyItem( final String name, final boolean setItemId ) { AdventureResult item = new AdventureResult( AdventureResult.NO_PRIORITY, name ); item.priority = AdventureResult.ITEM_PRIORITY; item.id = setItemId ? ItemDatabase.getItemId( name, 1, false ) : -1; return item; } public static final AdventureResult tallyItem( final String name, final int itemId ) { return new AdventureResult( name, itemId, 1, false ); } public static final AdventureResult tallyItem( final String name, final int count, final boolean setItemId ) { AdventureResult item = AdventureResult.tallyItem( name, setItemId ); item.count = count; return item; } /** * Accessor method to determine if this result is a status effect. * * @return <code>true</code> if this result represents a status effect */ public boolean isStatusEffect() { return this.priority == AdventureResult.EFFECT_PRIORITY; } /** * Accessor method to determine if this result is a muscle gain. * * @return <code>true</code> if this result represents muscle subpoint gain */ public boolean isMuscleGain() { return false; // overriden in subclass } /** * Accessor method to determine if this result is a mysticality gain. * * @return <code>true</code> if this result represents mysticality subpoint gain */ public boolean isMysticalityGain() { return false; // overriden in subclass } /** * Accessor method to determine if this result is a muscle gain. * * @return <code>true</code> if this result represents muscle subpoint gain */ public boolean isMoxieGain() { return false; // overriden in subclass } /** * Accessor method to determine if this result is an item, as opposed to meat, drunkenness, adventure or substat * gains. * * @return <code>true</code> if this result represents an item */ public boolean isItem() { return this.priority == AdventureResult.ITEM_PRIORITY; } public boolean isBountyItem() { return this.priority == AdventureResult.BOUNTY_ITEM_PRIORITY; } public boolean isMeat() { return this.priority == AdventureResult.MEAT_PRIORITY; } public boolean isMP() { return this.name.equals( AdventureResult.MP ); } public boolean isMonster() { return this.priority == AdventureResult.MONSTER_PRIORITY; } /** * Accessor method to retrieve the name associated with the result. * * @return The name of the result */ public String getName() { if ( !this.isItem() ) { return this.name; } switch ( this.id ) { case ItemPool.DUSTY_BOTTLE_OF_MERLOT: case ItemPool.DUSTY_BOTTLE_OF_PORT: case ItemPool.DUSTY_BOTTLE_OF_PINOT_NOIR: case ItemPool.DUSTY_BOTTLE_OF_ZINFANDEL: case ItemPool.DUSTY_BOTTLE_OF_MARSALA: case ItemPool.DUSTY_BOTTLE_OF_MUSCAT: return ConsumablesDatabase.dustyBottleName( this.id ); case ItemPool.MILKY_POTION: case ItemPool.SWIRLY_POTION: case ItemPool.BUBBLY_POTION: case ItemPool.SMOKY_POTION: case ItemPool.CLOUDY_POTION: case ItemPool.EFFERVESCENT_POTION: case ItemPool.FIZZY_POTION: case ItemPool.DARK_POTION: case ItemPool.MURKY_POTION: return AdventureResult.bangPotionName( this.id ); case ItemPool.VIAL_OF_RED_SLIME: case ItemPool.VIAL_OF_YELLOW_SLIME: case ItemPool.VIAL_OF_BLUE_SLIME: case ItemPool.VIAL_OF_ORANGE_SLIME: case ItemPool.VIAL_OF_GREEN_SLIME: case ItemPool.VIAL_OF_VIOLET_SLIME: case ItemPool.VIAL_OF_VERMILION_SLIME: case ItemPool.VIAL_OF_AMBER_SLIME: case ItemPool.VIAL_OF_CHARTREUSE_SLIME: case ItemPool.VIAL_OF_TEAL_SLIME: case ItemPool.VIAL_OF_INDIGO_SLIME: case ItemPool.VIAL_OF_PURPLE_SLIME: return AdventureResult.slimeVialName( this.id ); case ItemPool.PUNCHCARD_ATTACK: case ItemPool.PUNCHCARD_REPAIR: case ItemPool.PUNCHCARD_BUFF: case ItemPool.PUNCHCARD_MODIFY: case ItemPool.PUNCHCARD_BUILD: case ItemPool.PUNCHCARD_TARGET: case ItemPool.PUNCHCARD_SELF: case ItemPool.PUNCHCARD_FLOOR: case ItemPool.PUNCHCARD_DRONE: case ItemPool.PUNCHCARD_WALL: case ItemPool.PUNCHCARD_SPHERE: return AdventureResult.punchCardName( this.id ); default: return this.name; } } public String getDataName() { return this.name; } public String getPluralName( final int count ) { return count == 1 ? this.getName() : this.priority == AdventureResult.BOUNTY_ITEM_PRIORITY ? BountyDatabase.getPlural( this.getName() ) : this.id == -1 ? this.getName() + "s" : ItemDatabase.getPluralName( this.id ); } /** * Accessor method to retrieve the item Id associated with the result, if this is an item and the item Id is known. * * @return The item Id associated with this item */ public int getItemId() { if ( this.priority == AdventureResult.ITEM_PRIORITY ) { return this.id; } return -1; } public int getEffectId() { if ( this.priority == AdventureResult.EFFECT_PRIORITY ) { return this.id; } return -1; } /** * Accessor method to retrieve the total value associated with the result. In the event of substat points, this * returns the total subpoints within the <code>AdventureResult</code>; in the event of an item or meat gains, * this will return the total number of meat/items in this result. * * @return The amount associated with this result */ public int getCount() { return count; } public int[] getCounts() { // This should be called on multi-valued subclasses only! return null; } /** * Accessor method to retrieve the total value associated with the result stored at the given index of the count * array. * * @return The total value at the given index of the count array */ public int getCount( final int index ) { return index != 0 ? 0 : this.count; } /** * A static final method which parses the given string for any content which might be applicable to an * <code>AdventureResult</code>, and returns the resulting <code>AdventureResult</code>. * * @param s The string suspected of being an <code>AdventureResult</code> * @return An <code>AdventureResult</code> with the appropriate data * @throws NumberFormatException The string was not a recognized <code>AdventureResult</code> * @throws ParseException The value enclosed within parentheses was not a number. */ public static final AdventureResult parseResult( final String s ) { // If this result has been screwed up with Rad Libs, can't do anything with it. if ( s.startsWith( "You  " ) ) { return null; } if ( s.startsWith( "You gain" ) || s.startsWith( "You lose" ) || s.startsWith( "You spent" ) ) { // A stat has been modified - now you figure out which // one it was, how much it's been modified by, and // return the appropriate value StringTokenizer parsedGain = new StringTokenizer( s, " ." ); if ( parsedGain.countTokens() < 4 ) { return null; } parsedGain.nextToken(); // Skip "You" // Decide if the quantity increases or decreases int sign = parsedGain.nextToken().startsWith( "gain" ) ? 1 : -1; // Make sure we are looking at a number String val = parsedGain.nextToken(); if ( val.equals( "no" ) ) { val = "0"; } if ( !StringUtilities.isNumeric( val ) ) { return null; } // Yes. It is safe to parse it as an integer int modifier = sign * StringUtilities.parseInt( val ); // Stats actually fall into one of four categories - // simply pick the correct one and return the result. String statname = parsedGain.nextToken(); if ( statname.startsWith( "Adv" ) ) { return new AdventureResult( AdventureResult.ADV, modifier ); } if ( statname.startsWith( "Dru" ) ) { return new AdventureResult( AdventureResult.DRUNK, modifier ); } if ( statname.startsWith( "Full" ) ) { return new AdventureResult( AdventureResult.FULL, modifier ); } if ( statname.startsWith( "Me" ) ) { // "Meat" or "Meets", if Can Has Cyborger return new AdventureResult( AdventureResult.MEAT, modifier ); } if ( statname.startsWith( "addit" ) ) { statname = parsedGain.nextToken(); } if ( statname.startsWith( "PvP" ) ) { return new AdventureResult( AdventureResult.PVP, modifier ); } if ( parsedGain.hasMoreTokens() ) { char identifier = statname.charAt( 0 ); return new AdventureResult( identifier == 'H' || identifier == 'h' ? AdventureResult.HP : AdventureResult.MP, modifier ); } // In the current implementations, all stats gains are // located inside of a generic AdventureResult which // indicates how much of each substat is gained. int[] gained = { AdventureResult.MUS_SUBSTAT.contains( statname ) ? modifier : 0, AdventureResult.MYS_SUBSTAT.contains( statname ) ? modifier : 0, AdventureResult.MOX_SUBSTAT.contains( statname ) ? modifier : 0 }; return new AdventureMultiResult( AdventureResult.SUBSTATS, gained ); } return AdventureResult.parseItem( s, false ); } public static final AdventureResult parseItem( final String s, final boolean pseudoAllowed ) { StringTokenizer parsedItem = new StringTokenizer( s, "()" ); StringBuilder nameBuilder = new StringBuilder( parsedItem.nextToken().trim() ); int count = 1; while ( parsedItem.hasMoreTokens() ) { String next = parsedItem.nextToken().trim(); if ( !parsedItem.hasMoreTokens() && StringUtilities.isNumeric( next ) ) { count = StringUtilities.parseInt( next ); } else if ( !next.equals( "" ) ) { nameBuilder.append( " (" + next + ")" ); } } String name = nameBuilder.toString(); if ( !pseudoAllowed ) { return new AdventureResult( name, count ); } // Hand craft an item Adventure Result, regardless of the name AdventureResult item = new AdventureResult( AdventureResult.NO_PRIORITY, name ); item.priority = AdventureResult.ITEM_PRIORITY; item.id = ItemDatabase.getItemId( name, 1, false ); item.count = count; if ( item.id > 0 ) { // normalize name item.name = ItemDatabase.getItemDataName( item.id ); } return item; } /** * Converts the <code>AdventureResult</code> to a <code>String</code>. This is especially useful in debug, or * if the <code>AdventureResult</code> is to be displayed in a <code>ListModel</code>. * * @return The string version of this <code>AdventureResult</code> */ @Override public String toString() { if ( this.name == null ) { return "(Unrecognized result)"; } if ( this.priority == AdventureResult.MONSTER_PRIORITY ) { return this.name; } if ( this.name.equals( AdventureResult.ADV ) ) { return " Advs Used: " + KoLConstants.COMMA_FORMAT.format( this.count ); } if ( this.name.equals( AdventureResult.FREE_CRAFT ) ) { return " Free Crafts: " + KoLConstants.COMMA_FORMAT.format( this.count ); } if ( this.name.equals( AdventureResult.MEAT ) ) { return " Meat Gained: " + KoLConstants.COMMA_FORMAT.format( this.count ); } if ( this.name.equals( AdventureResult.MEAT_SPENT ) ) { return " Meat Spent: " + KoLConstants.COMMA_FORMAT.format( this.count ); } if ( this.name.equals( AdventureResult.CHOICE ) ) { return " Choices Left: " + KoLConstants.COMMA_FORMAT.format( this.count ); } if ( this.name.equals( AdventureResult.AUTOSTOP ) ) { return " Autostops Left: " + KoLConstants.COMMA_FORMAT.format( this.count ); } if ( this.name.equals( AdventureResult.PULL ) ) { return " Budgeted Pulls: " + KoLConstants.COMMA_FORMAT.format( this.count ); } if ( this.name.equals( AdventureResult.STILL ) ) { return " Still Usages: " + KoLConstants.COMMA_FORMAT.format( this.count ); } if ( this.name.equals( AdventureResult.TOME ) ) { return " Tome Summons: " + KoLConstants.COMMA_FORMAT.format( this.count ); } if ( this.name.equals( AdventureResult.EXTRUDE ) ) { return " Source Terminal Extrudes: " + KoLConstants.COMMA_FORMAT.format( this.count ); } if ( this.name.equals( AdventureResult.HP ) || this.name.equals( AdventureResult.MP ) || this.name.equals( AdventureResult.DRUNK ) || this.name.equals( AdventureResult.FULL ) ) { return " " + this.name + ": " + KoLConstants.COMMA_FORMAT.format( this.count ); } String name = this.getName(); if ( this.priority == AdventureResult.EFFECT_PRIORITY ) { if ( name.equals( "On the Trail" ) ) { String monster = Preferences.getString( "olfactedMonster" ); if ( !monster.equals( "" ) ) { name = name + " [" + monster + "]"; } } else { String skillName = UneffectRequest.effectToSkill( name ); if ( SkillDatabase.contains( skillName ) ) { int skillId = SkillDatabase.getSkillId( skillName ); if ( SkillDatabase.isAccordionThiefSong( skillId ) ) { name = "\u266B " + name; } if ( SkillDatabase.isExpression( skillId ) ) { name = "\u263A " + name; } } } } int count = this.count; return count == 1 ? name : count > Integer.MAX_VALUE/2 ? name + " (\u221E)" : count == PurchaseRequest.MAX_QUANTITY ? name + " (unlimited)" : name + " (" + KoLConstants.COMMA_FORMAT.format( count ) + ")"; } public String getConditionType() { if ( this.name == null ) { return ""; } if ( this.priority == AdventureResult.PSEUDO_ITEM_PRIORITY ) { return this.name.toLowerCase(); } if ( this.priority == AdventureResult.BOUNTY_ITEM_PRIORITY ) { return this.name.toLowerCase(); } if ( this.priority == AdventureResult.SUBSTAT_PRIORITY || this.priority == AdventureResult.FULLSTAT_PRIORITY ) { return "substats"; } if ( this.name.equals( AdventureResult.ADV ) || this.name.equals( AdventureResult.CHOICE ) ) { return "choiceadv"; } if ( this.name.equals( AdventureResult.AUTOSTOP ) ) { return "autostop"; } if ( this.name.equals( AdventureResult.MEAT ) ) { return "meat"; } if ( this.name.equals( AdventureResult.HP ) ) { return "health"; } if ( this.name.equals( AdventureResult.MP ) ) { return "mana"; } if ( this.name.equals( AdventureResult.FACTOID ) ) { return "factoid"; } if ( this.name.equals( AdventureResult.FLOUNDRY ) ) { return "floundry fish"; } if ( this.name.equals( AdventureResult.PIRATE_INSULT ) ) { return "pirate insult"; } if ( this.name.equals( AdventureResult.ARENA_ML ) ) { return "Arena flyer ML"; } if ( this.name.equals( AdventureResult.CHASM_BRIDGE ) ) { return "Chasm Bridge Progress"; } return "item"; } public String toConditionString() { if ( this.name == null ) { return ""; } String conditionType = this.getConditionType(); if ( !conditionType.equals( "item" ) ) { return this.count + " " + conditionType; } return "+" + this.count + " " + this.name.replaceAll( "[,\"]", "" ); } /** * Compares the <code>AdventureResult</code> with the given object for equality. * * @param o The <code>Object</code> to be compared with this <code>AdventureResult</code> * @return <code>true</code> if the <code>Object</code> is an <code>AdventureResult</code> * and has the same name as this one */ @Override public boolean equals( final Object o ) { if ( !( o instanceof AdventureResult ) ) { return false; } if ( o instanceof WildcardResult ) { return o.equals( this ); } AdventureResult ar = (AdventureResult) o; return this.priority == ar.priority && this.id == ar.id && ( this.name == null || ar.name == null ? this.name == ar.name : this.name.equals( ar.name ) ); } @Override public int hashCode() { if ( this.name == null ) { return 0; } return this.name.hashCode(); } /** * Compares the <code>AdventureResult</code> with the given object for name equality and priority differences. * Return values are consistent with the rules laid out in {@link java.lang.Comparable#compareTo(Object)}. */ public int compareTo( final AdventureResult o ) { if ( o == null ) { throw new NullPointerException(); } if ( o == this ) { return 0; } if ( this.priority != o.priority ) { return this.priority - o.priority; } if ( this.name == null ) { return o.name == null ? 0 : 1; } if ( o.name == null ) { return -1; } if ( this.priority == EFFECT_PRIORITY ) { // Status effects have IDs and durations. Sort by // duration, or by name, if durations are equal. int countComparison = this.getCount() - o.getCount(); return countComparison != 0 ? countComparison : this.id != o.id ? this.name.compareToIgnoreCase( o.name ) : 0; } int nameComparison = this.name.compareToIgnoreCase( o.name ); return nameComparison != 0 ? nameComparison : this.id - o.id; } /** * Utility method used for adding a given <code>AdventureResult</code> to a tally of <code>AdventureResult</code>s. * * @param tally The tally accumulating <code>AdventureResult</code>s * @param result The result to add to the tally */ public static final void addResultToList( final List<AdventureResult> sourceList, final AdventureResult result ) { int index = sourceList.indexOf( result ); // First, filter out things where it's a simple addition of an // item, or something which may not result in a change in the // state of the sourceList list. if ( index == -1 ) { if ( !result.isItem() ) { sourceList.add( result ); return; } int count = result.getCount(); if ( count == 0 ) return; if ( count < 0 && ( sourceList != KoLConstants.tally || !Preferences.getBoolean( "allowNegativeTally" ) ) ) { return; } sourceList.add( result ); if ( sourceList == KoLConstants.inventory ) { InventoryManager.fireInventoryChanged( result.getItemId() ); } return; } // These don't involve any addition -- ignore this entirely // for now. if ( result == GoalManager.GOAL_SUBSTATS ) { return; } // Compute the sum of the existing adventure result and the // current adventure result, and construct the sum. AdventureResult current = (AdventureResult) sourceList.get( index ); // Modify substats and fullstats in place if ( current instanceof AdventureMultiResult && result instanceof AdventureMultiResult ) { ((AdventureMultiResult) current).addResultInPlace( (AdventureMultiResult) result ); return; } AdventureResult sumResult = current.getInstance( current.count + result.count ); // Check to make sure that the result didn't transform the value // to zero - if it did, then remove the item from the list if // it's an item (non-items are exempt). if ( sumResult.isItem() ) { if ( sumResult.getCount() == 0 ) { sourceList.remove( index ); if ( sourceList == KoLConstants.inventory ) { InventoryManager.fireInventoryChanged( result.getItemId() ); } return; } else if ( sumResult.getCount() < 0 && ( sourceList != KoLConstants.tally || !Preferences.getBoolean( "allowNegativeTally" ) ) ) { sourceList.remove( index ); if ( sourceList == KoLConstants.inventory ) { InventoryManager.fireInventoryChanged( result.getItemId() ); } return; } sourceList.set( index, sumResult ); if ( sourceList == KoLConstants.inventory ) { InventoryManager.fireInventoryChanged( result.getItemId() ); } return; } else if ( sumResult.getCount() == 0 && ( sumResult.isStatusEffect() || sumResult.getName().equals( AdventureResult.CHOICE ) || sumResult.getName().equals( AdventureResult.AUTOSTOP ) ) ) { sourceList.remove( index ); return; } else if ( sumResult.getCount() < 0 && sumResult.isStatusEffect() ) { sourceList.remove( index ); return; } sourceList.set( index, sumResult ); } public static final void addOrRemoveResultToList( final List<AdventureResult> sourceList, final AdventureResult result ) { int index = sourceList.indexOf( result ); if ( index == -1 ) { sourceList.add( result ); return; } AdventureResult current = (AdventureResult) sourceList.get( index ); AdventureResult sumResult = current.getInstance( current.count + result.count ); if ( sumResult.getCount() <= 0 ) { sourceList.remove( index ); return; } sourceList.set( index, sumResult ); } public static final void removeResultFromList( final List sourceList, final AdventureResult result ) { int index = sourceList.indexOf( result ); if ( index != -1 ) { sourceList.remove( index ); } } public AdventureResult getNegation() { if ( this.isItem() && this.id != -1 ) { return this.count == 0 ? this : new AdventureResult( this.id, 0 - this.count, false ); } else if ( this.isStatusEffect() && this.id != -1 ) { return this.count == 0 ? this : new AdventureResult( this.id, 0 - this.count, true ); } return this.getInstance( -this.count ); } public AdventureResult getInstance( final int quantity ) { if ( this.isItem() ) { if ( this.count == quantity ) { return this; } // Handle pseudo and tally items that override methods of AdventureResult AdventureResult item; try { item = (AdventureResult) this.clone(); } catch ( CloneNotSupportedException e ) { // This should not happen. Hope for the best. item = new AdventureResult( AdventureResult.NO_PRIORITY, this.name ); item.priority = AdventureResult.ITEM_PRIORITY; item.id = this.id; } item.count = quantity; return item; } if ( this.isStatusEffect() ) { AdventureResult effect; try { effect = (AdventureResult) this.clone(); } catch ( CloneNotSupportedException e ) { // This should not happen. Hope for the best. effect = new AdventureResult( AdventureResult.NO_PRIORITY, this.name ); effect.priority = AdventureResult.EFFECT_PRIORITY; effect.id = this.id; } effect.count = quantity; return effect; } return new AdventureResult( this.name, quantity ); } public AdventureResult getInstance( final int[] quantity ) { return this.getInstance( quantity[ 0 ] ); } /** * Special method which simplifies the constant use of indexOf and count retrieval. This makes intent more * transparent. */ public int getCount( final List<AdventureResult> list ) { int index = list.indexOf( this ); return index == -1 ? 0 : list.get( index ).getCount(); } public static AdventureResult findItem( final int itemId, final List<AdventureResult> list ) { for ( AdventureResult item : list ) { if ( item.getItemId() == itemId ) { return item; } } return null; } public static final String bangPotionName( final int itemId ) { String itemName = ItemDatabase.getItemDataName( itemId ); String effect = Preferences.getString( "lastBangPotion" + itemId ); if ( effect.equals( "" ) ) { return itemName; } return itemName + " of " + effect; } public static final String slimeVialName( final int itemId ) { String itemName = ItemDatabase.getItemDataName( itemId ); String effect = Preferences.getString( "lastSlimeVial" + itemId ); if ( effect.equals( "" ) ) { return itemName; } return itemName + ": " + effect; } public final String bangPotionAlias() { if ( this.isItem() ) { if ( this.id >= ItemPool.FIRST_BANG_POTION && this.id <= ItemPool.LAST_BANG_POTION ) { String effect = Preferences.getString( "lastBangPotion" + this.id ); if ( effect.equals( "" ) ) { return this.name; } return "potion of " + effect; } if ( this.id >= ItemPool.FIRST_SLIME_VIAL && this.id < ItemPool.LAST_SLIME_VIAL ) { String effect = Preferences.getString( "lastSlimeVial" + this.id ); if ( effect.equals( "" ) ) { return this.name; } return "vial of slime: " + effect; } } return this.name; } public final AdventureResult resolveBangPotion() { String name = this.name; if ( name.startsWith( "potion of " ) ) { String effect = name.substring( 10 ); for ( int itemId = ItemPool.FIRST_BANG_POTION; itemId <= ItemPool.LAST_BANG_POTION; ++itemId ) { String potion = Preferences.getString( "lastBangPotion" + itemId ); if ( !potion.equals( "" ) && name.endsWith( potion ) ) { return ItemPool.get( itemId, this.getCount() ); } } return this; } if ( name.startsWith( "vial of slime: " ) ) { String effect = name.substring( 15 ); for ( int itemId = ItemPool.FIRST_SLIME_VIAL; itemId < ItemPool.LAST_SLIME_VIAL; ++itemId ) { String vial = Preferences.getString( "lastSlimeVial" + itemId ); if ( !vial.equals( "" ) && name.endsWith( vial ) ) { return ItemPool.get( itemId, this.getCount() ); } } return this; } return this; } public static final String punchCardName( final int itemId ) { for ( Object [] punchcard: ItemDatabase.PUNCHCARDS ) { if ( ( (Integer) punchcard[0]).intValue() == itemId ) { return (String) punchcard[2]; } } return ItemDatabase.getItemDataName( itemId ); } // AdventureMultiResult handles the specific stat-related result types // that must store multiple values, rather than having a 1-element count // array inside every AdventureResult. public static class AdventureMultiResult extends AdventureResult { private int[] counts; public AdventureMultiResult( final String name, final int[] counts ) { this( AdventureResult.choosePriority( name ), name, counts ); } protected AdventureMultiResult( final int subType, final String name, final int[] counts ) { super( subType, name, counts[ 0 ] ); this.counts = counts; } /** * Accessor method to determine if this result is a muscle gain. * * @return <code>true</code> if this result represents muscle subpoint gain */ @Override public boolean isMuscleGain() { return this.priority == AdventureResult.SUBSTAT_PRIORITY && this.counts[ 0 ] != 0; } /** * Accessor method to determine if this result is a mysticality gain. * * @return <code>true</code> if this result represents mysticality subpoint gain */ @Override public boolean isMysticalityGain() { return this.priority == AdventureResult.SUBSTAT_PRIORITY && this.counts[ 1 ] != 0; } /** * Accessor method to determine if this result is a muscle gain. * * @return <code>true</code> if this result represents muscle subpoint gain */ @Override public boolean isMoxieGain() { return this.priority == AdventureResult.SUBSTAT_PRIORITY && this.counts[ 2 ] != 0; } /** * Accessor method to retrieve the total value associated with the result. In the event of substat points, this * returns the total subpoints within the <code>AdventureResult</code>; in the event of an item or meat gains, * this will return the total number of meat/items in this result. * * @return The amount associated with this result */ @Override public int getCount() { int totalCount = 0; for ( int i = 0; i < this.counts.length; ++i ) { totalCount += this.counts[ i ]; } return totalCount; } @Override public int[] getCounts() { return this.counts; } /** * Accessor method to retrieve the total value associated with the result stored at the given index of the count * array. * * @return The total value at the given index of the count array */ @Override public int getCount( final int index ) { return index < 0 || index >= this.counts.length ? 0 : this.counts[ index ]; } /** * Converts the <code>AdventureResult</code> to a <code>String</code>. This is especially useful in debug, or * if the <code>AdventureResult</code> is to be displayed in a <code>ListModel</code>. * * @return The string version of this <code>AdventureResult</code> */ @Override public String toString() { if ( this.name.equals( AdventureResult.SUBSTATS ) || this.name.equals( AdventureResult.FULLSTATS ) ) { return " " + this.name + ": " + KoLConstants.COMMA_FORMAT.format( this.counts[ 0 ] ) + " / " + KoLConstants.COMMA_FORMAT.format( this.counts[ 1 ] ) + " / " + KoLConstants.COMMA_FORMAT.format( this.counts[ 2 ] ); } return "(Unrecognized multi-result)"; } @Override public String toConditionString() { if ( this.name.equals( AdventureResult.SUBSTATS ) ) { StringBuilder stats = new StringBuilder(); if ( this.counts[ 0 ] > 0 ) { stats.append( KoLCharacter.calculateBasePoints( KoLCharacter.getTotalMuscle() + this.counts[ 0 ] ) + " muscle" ); } if ( this.counts[ 1 ] > 0 ) { if ( this.counts[ 0 ] > 0 ) { stats.append( ", " ); } stats.append( KoLCharacter.calculateBasePoints( KoLCharacter.getTotalMysticality() + this.counts[ 1 ] ) + " mysticality" ); } if ( this.counts[ 2 ] > 0 ) { if ( this.counts[ 0 ] > 0 || this.counts[ 1 ] > 0 ) { stats.append( ", " ); } stats.append( KoLCharacter.calculateBasePoints( KoLCharacter.getTotalMoxie() + this.counts[ 2 ] ) + " moxie" ); } return stats.toString(); } return super.toConditionString(); } protected void addResultInPlace( AdventureMultiResult result ) { for ( int i = 0; i < this.counts.length; ++i ) { this.counts[ i ] += result.counts[ i ]; } } @Override public AdventureResult getNegation() { int[] newcounts = new int[ this.counts.length ]; for ( int i = 0; i < this.counts.length; ++i ) { newcounts[ i ] = 0 - this.counts[ i ]; } return this.getInstance( newcounts ); } @Override public AdventureResult getInstance( final int[] quantity ) { if ( this.priority == AdventureResult.SUBSTAT_PRIORITY ) { return new AdventureMultiResult( AdventureResult.SUBSTATS, quantity ); } return new AdventureMultiResult( AdventureResult.FULLSTATS, quantity ); } } public static class WildcardResult extends AdventureResult { // Note that these objects must not be placed in a sorted list, since they // are not meaningfully comparable other than via equals(). private String match; private String[] matches; private boolean negated; public WildcardResult( String name, int count, String match, boolean negated ) { super( AdventureResult.ITEM_PRIORITY, name, count ); this.match = match; this.matches = match.split( "\\s*[|/]\\s*" ); for ( int i = 0; i < matches.length; ++i ) { this.matches[ i ] = this.matches[ i ].toLowerCase(); } this.negated = negated; } @Override public AdventureResult getInstance( int count ) { return new WildcardResult( this.getName(), count, this.match, this.negated ); } @Override public boolean equals( final Object o ) { if ( !( o instanceof AdventureResult ) ) { return false; } boolean hasMatch = false; AdventureResult ar = (AdventureResult) o; String arName = ar.getName().toLowerCase(); for ( int i = 0; i < this.matches.length && !hasMatch; ++i ) { hasMatch = arName.indexOf( this.matches[ i ] ) != -1; } return hasMatch ^ this.negated; } @Override public int getCount( final List<AdventureResult> list ) { int count = 0; for ( AdventureResult ar : list ) { if ( this.equals( ar ) ) { count += ar.getCount(); } } return count; } @Override public void normalizeItemName() { // Overridden to avoid "unknown item found" messages. } public static WildcardResult getInstance( String text ) { if ( text.indexOf( "any" ) == -1 ) { return null; } String[] pieces = text.split( " ", 2 ); int count = StringUtilities.isNumeric( pieces[ 0 ] ) ? StringUtilities.parseInt( pieces[ 0 ] ) : 0; if ( pieces.length > 1 && count != 0 ) { text = pieces[ 1 ]; } else { count = 1; } if ( text.startsWith( "any " ) ) { return new WildcardResult( text, count, text.substring( 4 ), false ); } if ( text.startsWith( "anything but " ) ) { return new WildcardResult( text, count, text.substring( 13 ), true ); } return null; } } }