/**
* 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.util.List;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.java.dev.spellcast.utilities.SortedListModel;
import net.sourceforge.kolmafia.AdventureResult;
import net.sourceforge.kolmafia.KoLCharacter;
import net.sourceforge.kolmafia.KoLConstants;
import net.sourceforge.kolmafia.RequestLogger;
import net.sourceforge.kolmafia.listener.NamedListenerRegistry;
import net.sourceforge.kolmafia.objectpool.EffectPool;
import net.sourceforge.kolmafia.objectpool.IntegerPool;
import net.sourceforge.kolmafia.objectpool.ItemPool;
import net.sourceforge.kolmafia.request.GenericRequest;
import net.sourceforge.kolmafia.session.ChoiceManager;
import net.sourceforge.kolmafia.session.EquipmentManager;
import net.sourceforge.kolmafia.utilities.StringUtilities;
public class EdServantData
implements Comparable<EdServantData>
{
public static final AdventureResult CROWN_OF_ED = ItemPool.get( ItemPool.CROWN_OF_ED, 1 );
public static final AdventureResult PURR = EffectPool.get( EffectPool.PURR_OF_THE_FELINE );
public static final Pattern SID_PATTERN = Pattern.compile( "sid=(\\d+)" );
public static int extractServantId( final String urlString )
{
Matcher matcher = EdServantData.SID_PATTERN.matcher( urlString );
return matcher.find() ? StringUtilities.parseInt( matcher.group( 1 ) ) : 0;
}
public static final Object[][] SERVANTS =
{
// Servant type
// Canonical servant type
// Servant ID
// Image file name
// Level 1 Power
// Level 7 Power
// Level 14 Power
// Level 21 Power
{
"Cat",
StringUtilities.getCanonicalName( "Cat" ),
IntegerPool.get( 1 ),
"edserv1.gif",
"Gives unpleasant gifts",
"Helps find items",
"Lowers enemy stats",
"Teaches you how to find items",
},
{
"Belly-Dancer",
StringUtilities.getCanonicalName( "Belly-Dancer" ),
IntegerPool.get( 2 ),
"edserv2.gif",
"Lowers enemy stats",
"Restores MP",
"Picks pockets",
"Teaches you how to restore MP",
},
{
"Maid",
StringUtilities.getCanonicalName( "Maid" ),
IntegerPool.get( 3 ),
"edserv3.gif",
"Helps find meat",
"Attacks enemies",
"Prevents enemy attacks",
"Teaches you how to find meat",
},
{
"Bodyguard",
StringUtilities.getCanonicalName( "Bodyguard" ),
IntegerPool.get( 4 ),
"edserv4.gif",
"Prevents enemy attacks",
"Attacks enemies",
"Attacks enemies even when guarding",
"Teaches you how to defend yourself",
},
{
"Scribe",
StringUtilities.getCanonicalName( "Scribe" ),
IntegerPool.get( 5 ),
"edserv5.gif",
"Improves stat gains",
"Improves spell crit",
"Improves spell damage",
"Teaches you how to improve stat gains",
},
{
"Priest",
StringUtilities.getCanonicalName( "Priest" ),
IntegerPool.get( 6 ),
"edserv6.gif",
"Attacks undead enemies",
"Improves evocation spells",
"Improves Ka drops",
"Teaches you how to improve spell damage",
},
{
"Assassin",
StringUtilities.getCanonicalName( "Assassin" ),
IntegerPool.get( 7 ),
"edserv7.gif",
"Attacks enemies",
"Lowers enemy stats",
"Staggers enemies",
"Teaches you how to improve physical attacks",
},
};
public static final String [] SERVANT_ARRAY = new String[ EdServantData.SERVANTS.length ];
public static final String [] CANONICAL_SERVANT_ARRAY = new String[ EdServantData.SERVANTS.length ];
static
{
for ( int i = 0; i < EdServantData.SERVANT_ARRAY.length; ++i )
{
Object [] data = EdServantData.SERVANTS[ i ];
EdServantData.SERVANT_ARRAY[ i ] = EdServantData.dataToType( data );
EdServantData.CANONICAL_SERVANT_ARRAY[ i ] = EdServantData.dataToCanonicalType( data );
}
Arrays.sort( EdServantData.SERVANT_ARRAY );
};
public static String dataToType( Object[] data )
{
return data == null ? "" : (String)data[ 0 ];
}
public static String dataToCanonicalType( Object[] data )
{
return data == null ? "" : (String)data[ 1 ];
}
public static int dataToId( Object[] data )
{
return data == null ? 0 : ((Integer)data[ 2 ]).intValue();
}
public static String dataToImage( Object[] data )
{
return data == null ? null : (String)data[ 3 ];
}
public static String dataToLevel1Ability( Object[] data )
{
return data == null ? null : (String)data[ 4 ];
}
public static String dataToLevel7Ability( Object[] data )
{
return data == null ? null : (String)data[ 5 ];
}
public static String dataToLevel14Ability( Object[] data )
{
return data == null ? null : (String)data[ 6 ];
}
public static String dataToLevel21Ability( Object[] data )
{
return data == null ? null : (String)data[ 7 ];
}
public static Object[] idToData( final int id )
{
for ( Object[] data : EdServantData.SERVANTS )
{
if ( EdServantData.dataToId( data ) == id )
{
return data;
}
}
return null;
}
public static final List<String> getMatchingNames( final String substring )
{
return StringUtilities.getMatchingNames( EdServantData.CANONICAL_SERVANT_ARRAY, substring );
}
public static Object[] typeToData( final String type )
{
// Do fuzzy matching
List<String> matchingNames = EdServantData.getMatchingNames( type );
if ( matchingNames.size() != 1 )
{
return null;
}
String name = matchingNames.get( 0 );
for ( Object[] data : EdServantData.SERVANTS )
{
if ( name.equals( EdServantData.dataToCanonicalType( data ) ) )
{
return data;
}
}
return null;
}
public static String idToType( final int id )
{
return id == 0 ? "(none)" : EdServantData.dataToType( EdServantData.idToData( id ) );
}
public static final EdServantData NO_SERVANT = new EdServantData( 0 );
private Object[] data;
private final String type;
private final int id;
private String name;
private int experience;
private int level;
public static final SortedListModel<EdServantData> edServants = new SortedListModel<EdServantData>();
public static EdServantData currentEdServant = EdServantData.NO_SERVANT;
public static void initialize()
{
EdServantData.edServants.clear();
EdServantData.edServants.add( EdServantData.NO_SERVANT );
EdServantData.currentEdServant = EdServantData.NO_SERVANT;
}
private EdServantData( final Object [] data )
{
this.data = data;
this.type = EdServantData.dataToType( data );
this.id = EdServantData.dataToId( data );
this.name = "";
this.experience = 0;
this.level = 0;
}
private EdServantData( final String type )
{
this( EdServantData.typeToData( type ) );
}
private EdServantData( final int id )
{
this( EdServantData.idToData( id ) );
}
public Object [] getData()
{
return this.data;
}
public String getType()
{
return this.type;
}
public int getId()
{
return this.id;
}
public String getName()
{
return this.name;
}
public final void setName( final String name )
{
this.name = name;
}
public int getLevel()
{
int level = this.level;
if ( KoLConstants.activeEffects.contains( EdServantData.PURR ) ) level += 5;
return level;
}
public int getExperience()
{
return this.experience;
}
public String getImage()
{
return this.data == null ? "" : EdServantData.dataToImage( this.data );
}
public String getLevel1Ability()
{
return this.data == null ? "" : EdServantData.dataToLevel1Ability( this.data );
}
public String getLevel7Ability()
{
return this.data == null ? "" : EdServantData.dataToLevel7Ability( this.data );
}
public String getLevel14Ability()
{
return this.data == null ? "" : EdServantData.dataToLevel14Ability( this.data );
}
public String getLevel21Ability()
{
return this.data == null ? "" : EdServantData.dataToLevel21Ability( this.data );
}
@Override
public String toString()
{
return this.id == 0 ? "(none)" : this.name + ", the " + this.type + " (lvl. " + this.getLevel() + ", " + this.getExperience() + " xp)";
}
@Override
public boolean equals( final Object o )
{
return o != null && o instanceof EdServantData && this.id == ( (EdServantData) o ).id;
}
@Override
public int hashCode()
{
return this.id;
}
public int compareTo( final EdServantData td )
{
return this.id - td.id;
}
// <b>Busy Servant</b>: <img src=http://images.kingdomofloathing.com/itemimages/edserv2.gif>Hetekhemerit, the Belly-Dancer (lvl. 11, 142 XP) <small>[<a href=# id="ren">rename</a>]</small><br><span class=tiny> <font color=blue><B>Level 1:</b> Lowers enemy stats</font><br> <font color=blue><b>Level 7:</b> Restores MP</font><br> <font color=gray><b>Level 14:</b> Picks pockets</font><br> <font color=gray><b>Level 21:</b> Teaches you how to restore MP</font><br></span><div style="display: none" id="rename"><form method="post" action="choice.php" style="display:inline"><input type="hidden" name="whichchoice" value="1053" /><input type="hidden" name="option" value="2" /><input type="hidden" name="pwd" value="a4ce8559175f86f50b2d8b7626826e3e" /><input type="hidden" name="sid" value="2" /><input type="text" size="30" maxlen="30" name="name" value="Hetekhemerit" /><input type="submit" class="button" value="Rename" /></form></div>
private static final Pattern BUSY_PATTERN =
Pattern.compile( "<b>Busy Servant</b>: <img.*?/itemimages/(edserv\\d.gif)>(.*?), the (.*?) \\(lvl. (\\d+), ([\\d,]+) XP\\)", Pattern.DOTALL );
// <b>Freed, but Lazy Servants</b><table>...</table>
private static final Pattern FREED_TABLE_PATTERN =
Pattern.compile( "<b>Freed, but Lazy Servants</b><table>(.*?)</table>", Pattern.DOTALL );
// <tr><td valign=top><img src=http://images.kingdomofloathing.com/itemimages/edserv1.gif></td><td>Hethys, the Cat<br><span class=tiny> <font color=blue><B>Level 1:</b> Gives unpleasant gifts</font><br> <font color=blue><b>Level 7:</b> Helps find items</font><br> <font color=blue><b>Level 14:</b> Lowers enemy stats</font><br> <font color=gray><b>Level 21:</b> Teaches you how to find items</font><br></span></td><td valign=top>(level 14, 221 xp)</td><td valign=top><form method="post" action="choice.php" style="display:inline"><input type="hidden" name="whichchoice" value="1053" /><input type="hidden" name="option" value="1" /><input type="hidden" name="pwd" value="a4ce8559175f86f50b2d8b7626826e3e" /><input type="hidden" name="sid" value="1" /><input type="submit" class="button" value="Put to Work" /></form></td></tr>
private static final Pattern FREED_PATTERN =
Pattern.compile( "<img.*?/itemimages/(edserv\\d.gif)></td><td>(.*?), the (.*?)<br>.*?\\(level (\\d+), ([\\d,]+) xp\\)", Pattern.DOTALL );
// <b>Entombed Servants</b> - You may release 0 more servants.<table>...</table>
// <tr><td valign=top><img src=http://images.kingdomofloathing.com/itemimages/edserv4.gif></td><td>Bodyguard<br><span class=tiny> <font color=gray><B>Level 1:</b> Prevents enemy attacks</font><br> <font color=gray><b>Level 7:</b> Attacks enemies</font><br> <font color=gray><b>Level 14:</b> Attacks enemies even when guarding</font><br> <font color=gray><b>Level 21:</b> Teaches you how to defend yourself</font><br></span></td></tr>
public static final void inspectServants( final String responseText )
{
// Assume you have no servant
EdServantData current = EdServantData.NO_SERVANT;
if ( responseText.contains( "Busy Servant" ) )
{
Matcher busyMatcher = EdServantData.BUSY_PATTERN.matcher( responseText );
if ( busyMatcher.find() )
{
current = EdServantData.registerEdServant( busyMatcher );
}
}
Matcher freedTableMatcher = EdServantData.FREED_TABLE_PATTERN.matcher( responseText );
if ( freedTableMatcher.find() )
{
Matcher freedMatcher = EdServantData.FREED_PATTERN.matcher( freedTableMatcher.group( 1 ) );
while ( freedMatcher.find() )
{
EdServantData servant = EdServantData.registerEdServant( freedMatcher );
}
}
if ( current != EdServantData.currentEdServant )
{
EdServantData.currentEdServant = current;
KoLCharacter.recalculateAdjustments();
KoLCharacter.updateStatus();
}
}
public static final EdServantData currentServant()
{
return EdServantData.currentEdServant;
}
public static final List<EdServantData> getServants()
{
return EdServantData.edServants;
}
public final void addCombatExperience( String responseText )
{
// As far as I know:
// - once you select your first servant, you can never NOT have one
// (but you might not be Ed or might not have selected a servant yet)
if ( this == EdServantData.NO_SERVANT )
{
return;
}
// - a servant's experience caps at 441 (level 21)
if ( this.experience < 441 )
{
// - a servant gains 1 XP every time you win a fight
// - (if you are wearing the Crown of Ed the Undying, they gain 2)
// - they level up when their XP hits the square of the level
// - each servant has a unique "this servant leveled up" message.
// (which is cute, but we can derive level from experience)
int next = this.level + 1;
int delta = KoLCharacter.hasEquipped( EdServantData.CROWN_OF_ED, EquipmentManager.HAT ) ? 2 : 1;
this.experience = Math.min( this.experience + delta, 441 );
if ( this.experience >= ( next * next ) )
{
++this.level;
}
}
}
public static final EdServantData findEdServant( final String type )
{
for ( EdServantData servant : EdServantData.edServants )
{
if ( servant.type.equals( type ) )
{
return servant;
}
}
return null;
}
public static final EdServantData findEdServantById( final int id )
{
for ( EdServantData servant : EdServantData.edServants )
{
if ( servant.id == id )
{
return servant;
}
}
return null;
}
private static final EdServantData registerEdServant( final Matcher matcher )
{
String type = matcher.group( 3 );
EdServantData servant = EdServantData.findEdServant( type );
if ( servant == null )
{
// Add new familiar to list
servant = new EdServantData( type );
EdServantData.edServants.add( servant );
}
// String image = matcher.group( 1 );
String name = matcher.group( 2 );
int level = StringUtilities.parseInt( matcher.group( 4 ) );
int experience = StringUtilities.parseInt( matcher.group( 5 ) );
servant.name = new String( name );
servant.level = level;
servant.experience = experience;
return servant;
}
private static final AdventureResult PURR_OF_THE_FELINE = EffectPool.get( EffectPool.PURR_OF_THE_FELINE );
public static final void setEdServant( final Matcher matcher )
{
int id = StringUtilities.parseInt( matcher.group( 3 ) );
EdServantData servant = EdServantData.findEdServantById( id );
if ( servant == null )
{
return;
}
String name = matcher.group( 1 );
int level = StringUtilities.parseInt( matcher.group( 2 ) );
servant.name = new String( name );
servant.level = level - ( KoLConstants.activeEffects.contains( EdServantData.PURR_OF_THE_FELINE ) ? 5 : 0 );
if ( servant != EdServantData.currentEdServant )
{
EdServantData.currentEdServant = servant;
KoLCharacter.recalculateAdjustments();
KoLCharacter.updateStatus();
}
}
public static final void setEdServant( final String type )
{
EdServantData servant = EdServantData.findEdServant( type );
if ( servant != EdServantData.currentEdServant )
{
EdServantData.currentEdServant = servant;
KoLCharacter.recalculateAdjustments();
KoLCharacter.updateStatus();
}
}
public static final void manipulateServants( final GenericRequest request, final String responseText )
{
// This should be a response to choice #1053
String urlString = request.getURLString();
if ( !urlString.startsWith( "choice.php" ) || !urlString.contains( "whichchoice=1053" ) )
{
return;
}
int decision = ChoiceManager.extractOptionFromURL( urlString );
if ( decision == 0 )
{
return;
}
// We did ... something. Re-parse servants.
EdServantData.inspectServants( responseText );
NamedListenerRegistry.fireChange( "(edservants)" );
// choice.php?whichchoice=1053&option=3&pwd&sid=6
// choice.php?whichchoice=1053&option=5&pwd
int sid = EdServantData.extractServantId( urlString );
Object[] data = idToData( sid );
switch ( decision )
{
case 1: // Changing servant
case 3: // Freeing a servant
case 5: // Imparting wisdom to current servant
RequestLogger.updateSessionLog( "Current servant: " + EdServantData.currentEdServant );
break;
}
}
public static final boolean registerRequest( final String urlString )
{
// We know that we are submitting ... something for choice 1053
int decision = ChoiceManager.extractOptionFromURL( urlString );
if ( decision == 0 )
{
// Log nothing if not taking a choice.
return true;
}
int sid = EdServantData.extractServantId( urlString );
Object[] data = idToData( sid );
String servant = data == null ? "(unknown servant)" : EdServantData.dataToType( data );
switch ( decision )
{
case 1:
// Changing servant
RequestLogger.updateSessionLog( "Putting your " + servant + " to work" );
break;
case 3:
// Releasing a servant
RequestLogger.updateSessionLog( "Releasing your " + servant );
break;
case 5:
// Imparting wisdom to current servant
RequestLogger.updateSessionLog( "Imparting wisdom to " + EdServantData.currentEdServant );
break;
default:
return false;
}
return true;
}
}