/** * 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.webui; import java.util.regex.Matcher; import net.sourceforge.kolmafia.KoLCharacter; import net.sourceforge.kolmafia.KoLConstants; import net.sourceforge.kolmafia.KoLmafia; import net.sourceforge.kolmafia.MonsterData; import net.sourceforge.kolmafia.RequestLogger; import net.sourceforge.kolmafia.combat.MonsterStatusTracker; import net.sourceforge.kolmafia.persistence.SkillDatabase; import net.sourceforge.kolmafia.preferences.Preferences; import net.sourceforge.kolmafia.request.FightRequest; import net.sourceforge.kolmafia.utilities.StringUtilities; public class DiscoCombatHelper { // Only Disco Bandits can do combos. public static boolean canCombo; private static final int UNKNOWN = -1; public static final int BREAK_IT_ON_DOWN = 0; public static final int POP_AND_LOCK_IT = 1; public static final int RUN_LIKE_THE_WIND = 2; public static final int FIRST_RAVE_SKILL = 0; public static final int LAST_RAVE_SKILL = 2; public static final int NUM_SKILLS = 3; public static final String [] SKILLS = new String[] { "Break It On Down", "Pop and Lock It", "Run Like the Wind", }; public static final int [] SKILL_ID = new int[] { 50, 51, 52, }; public static final String [] BUTTON_NAME = new String[] { "Break", "Pop", "Run", }; public static final boolean [] knownSkill = new boolean[ NUM_SKILLS ]; public static final int RAVE_CONCENTRATION = 0; public static final int RAVE_NIRVANA = 1; public static final int RAVE_KNOCKOUT = 2; public static final int RAVE_BLEEDING = 3; public static final int RAVE_STEAL = 4; public static final int RAVE_SUBSTATS = 5; public static final int RANDOM_RAVE = 6; public static final int FIRST_RAVE_COMBO = 0; public static final int NUM_COMBOS = 7; public static final String [][] COMBOS = { { "Rave Concentration", "Item Drop +30", }, { "Rave Nirvana", "Meat Drop +50", }, { "Rave Knockout", "Multi-round stun+damage", }, { "Rave Bleeding", "Recurring damage", }, { "Rave Steal", "Steal item", }, { "Rave Substats", "2-4 substats", }, { "Random Rave", "Learn a new combo!", }, }; public static final boolean [] knownCombo = new boolean[ NUM_COMBOS ]; private static int[][][] COMBO_SKILLS = { // Rave Concentration { { UNKNOWN }, { UNKNOWN }, { UNKNOWN }, }, // Rave Nirvana { { UNKNOWN }, { UNKNOWN }, { UNKNOWN }, }, // Rave Knockout { { UNKNOWN }, { UNKNOWN }, { UNKNOWN }, }, // Rave Bleeding { { UNKNOWN }, { UNKNOWN }, { UNKNOWN }, }, // Rave Steal { { UNKNOWN }, { UNKNOWN }, { UNKNOWN }, }, // Rave Substats { { UNKNOWN }, { UNKNOWN }, { UNKNOWN }, }, // Random Rave { { UNKNOWN }, { UNKNOWN }, { UNKNOWN }, }, }; // Count of disco skills used in sequence private static int counter = 0; private static final int [] sequence = new int[3]; public static final void initialize() { DiscoCombatHelper.canCombo = KoLCharacter.getClassType().equals( KoLCharacter.DISCO_BANDIT ); if ( !DiscoCombatHelper.canCombo ) { return; } for ( int i = 0; i < NUM_SKILLS; ++i ) { String name = SKILLS[ i ]; knownSkill[ i ] = KoLCharacter.hasSkill( name ); } for ( int i = 0; i < NUM_COMBOS; ++i ) { DiscoCombatHelper.checkCombo( i ); } DiscoCombatHelper.counter = 0; DiscoCombatHelper.sequence[ 0 ] = 0; DiscoCombatHelper.sequence[ 1 ] = 0; DiscoCombatHelper.sequence[ 2 ] = 0; } private static int skillIdToSkill( final String skill ) { return DiscoCombatHelper.skillIdToSkill( StringUtilities.parseInt( skill ) ); } private static int skillIdToSkill( final int skill ) { for ( int i = 0; i < NUM_SKILLS; ++i ) { if ( skill == SKILL_ID[i] ) { return i; } } return -1; } private static int skillNameToSkill( final String name ) { for ( int i = 0; i < NUM_SKILLS; ++i ) { if ( SKILLS[i].equals( name ) ) { return i; } } return -1; } public static boolean canRaveSteal() { if ( Preferences.getInteger( "_raveStealCount" ) < 30 ) { return true; } // Rave Steal in the volcano island always works MonsterData monster = MonsterStatusTracker.getLastMonster(); if ( monster == null ) { return false; } String encounter = monster.getName(); if ( encounter.equals( "Breakdancing Raver" ) || encounter.equals( "Pop-and-Lock Raver" ) || encounter.equals( "Running Man" ) ) { return true; } return false; } public static String disambiguateCombo( String name ) { name = name.trim().toLowerCase(); for ( int i = 0; i < NUM_COMBOS; ++i ) { if ( COMBOS[ i ][ 0 ].toLowerCase().indexOf( name ) != -1 ) { return COMBOS[ i ][ 0 ]; } } return null; } public static int[] getCombo( String name ) { name = name.trim().toLowerCase(); for ( int i = 0; i < NUM_COMBOS; ++i ) { if ( COMBOS[ i ][ 0 ].toLowerCase().indexOf( name ) != -1 ) { return getCombo( i ); } } return null; } private static int[] getCombo( final int combo ) { if ( !DiscoCombatHelper.canCombo || !knownCombo[ combo ] ) { return null; } int[][] data = COMBO_SKILLS[ combo ]; int[] rv = new int[ data.length ]; for ( int i = 0; i < data.length; ++i ) { // Some combo allow multiple skills. Pick the first known one. int [] skills = data[i]; for ( int j = 0; j < skills.length; ++j ) { int skill = skills[ j ]; if ( knownSkill[ skill ] ) { rv[ i ] = SKILL_ID[ skill ]; break; } } } return rv; } private static final void checkCombo( final int combo ) { int [][] data = COMBO_SKILLS[ combo ]; // If it's a rave skill, we need to have learned it in battle. if ( combo == RANDOM_RAVE ) { int found = 0; findUnknownCombo: for ( int sel = 0; sel < 27; ++sel ) { int s1 = sel % 3 + FIRST_RAVE_SKILL; int s2 = (sel / 3) % 3 + FIRST_RAVE_SKILL; int s3 = (sel / 9) % 3 + FIRST_RAVE_SKILL; if ( s1 == s2 || s1 == s3 || s2 == s3 ) { continue; } for ( int test = FIRST_RAVE_COMBO; test < RANDOM_RAVE; ++test ) { int[][] testdata = COMBO_SKILLS[ test ]; if ( s1 == testdata[ 0 ][ 0 ] && s2 == testdata[ 1 ][ 0 ] && s3 == testdata[ 2 ][ 0 ] ) { continue findUnknownCombo; } } // We don't know this combo yet. if ( found == 1 ) { // If we've already found an unknown // combo, note that we found more than // one and stop looking. found = 2; break; } // Save first unknown combo data[ 0 ][ 0 ] = s1; data[ 1 ][ 0 ] = s2; data[ 2 ][ 0 ] = s3; found = 1; } // Check how many unknown combos there are switch ( found ) { case 1: // If there is only one unknown combo, we can // deduce what it is for ( int test = FIRST_RAVE_COMBO; test < RANDOM_RAVE; ++test ) { if ( !knownCombo[ test ] ) { KoLmafia.updateDisplay( "All rave combos have been identified!" ); DiscoCombatHelper.learnRaveCombo( test, data[ 0 ][ 0 ], data[ 1 ][ 0 ], data[ 2 ][ 0 ] ); break; } } // Fall through case 0: // We no longer need random rave knownCombo[ combo ] = false; return; } // There are at least two unknown combos and the skills // for the first one are set up in data } else if ( combo >= FIRST_RAVE_COMBO ) { String setting = "raveCombo" + String.valueOf( combo - FIRST_RAVE_COMBO + 1 ); String seq = Preferences.getString( setting ); String[] skills = seq.split( "," ); if ( skills.length == 3 ) { for ( int i = 0; i < skills.length; ++i ) { int skill = DiscoCombatHelper.skillNameToSkill( skills[ i ] ); if ( skill < FIRST_RAVE_SKILL || skill > LAST_RAVE_SKILL ) { knownCombo[ combo ] = false; return; } data[i][0] = skill; } knownCombo[ combo ] = true; return; } knownCombo[ combo ] = false; return; } // Check that we know all the skills for ( int i = 0; i < data.length; ++i ) { // Some combo allow multiple skills. Any will do. int [] skills = data[i]; boolean known = false; for ( int j = 0; j < skills.length; ++j ) { int skill = skills[ j ]; if ( skill != UNKNOWN && knownSkill[ skill ] ) { known = true; break; } } // If we don't know a skill, give up. if ( !known ) { knownCombo[ combo ] = false; return; } } // We know the necessary skills knownCombo[ combo ] = true; } public static final void learnSkill( final String name ) { if ( !DiscoCombatHelper.canCombo ) { return; } boolean discoSkill = false; for ( int i = 0; i < NUM_SKILLS; ++i ) { if ( SKILLS[i].equals( name ) ) { discoSkill = true; knownSkill[ i ] = true; break; }; } // If it's not a Disco Bandit combat skill, no combo if ( !discoSkill ) { return; } for ( int i = 0; i < NUM_COMBOS; ++i ) { DiscoCombatHelper.checkCombo( i ); } } public static final void parseFightRound( final String action, final Matcher macroMatcher ) { if ( !DiscoCombatHelper.canCombo ) { return; } String responseText; try { responseText = macroMatcher.group(); } catch ( IllegalStateException e ) { // page structure is botched - should have already been reported return; } // Two of the Rave Combos we can learn show up in the next // round of battle, regardless of what we do this round. if ( DiscoCombatHelper.counter == 3 ) { // Your opponent seems to be temporarily unconscious if ( responseText.indexOf( "seems to be temporarily unconscious" ) != -1 ) { DiscoCombatHelper.learnRaveCombo( RAVE_KNOCKOUT ); } // He bleeds from various wounds you've inflicted if ( responseText.indexOf( "bleeds from various wounds you've inflicted" ) != -1 ) { DiscoCombatHelper.learnRaveCombo( RAVE_BLEEDING ); } } if ( action == null || !action.startsWith( "skill" ) ) { DiscoCombatHelper.counter = 0; return; } int skill = DiscoCombatHelper.skillIdToSkill( action.substring( 5 ) ); if ( skill < 0 ) { DiscoCombatHelper.counter = 0; return; } // Track last three disco skills used in sequence. int index = DiscoCombatHelper.counter; if ( index == 3 ) { // Shift skills back DiscoCombatHelper.sequence[ 0 ] = DiscoCombatHelper.sequence[ 1 ]; DiscoCombatHelper.sequence[ 1 ] = DiscoCombatHelper.sequence[ 2 ]; index = 2; } DiscoCombatHelper.sequence[index++] = skill; DiscoCombatHelper.counter = index; // If we have completed a known disco or rave combo, reset // A combo must have at least two skills. int combo = -1; for ( int i = 0; DiscoCombatHelper.counter > 1 && i < NUM_COMBOS; ++i ) { if ( !knownCombo[ i ] || i == RANDOM_RAVE ) { continue; } int [][] data = COMBO_SKILLS[ i ]; // If we have the correct number of skills to match // this sequence, check it. if ( DiscoCombatHelper.counter == data.length && DiscoCombatHelper.checkSequence( data, 0 ) ) { combo = i; DiscoCombatHelper.counter = 0; break; } // If we have three skills in a row, we can match // either a three-skill combo or a two-skill combo if ( DiscoCombatHelper.counter == 3 && data.length == 2 && DiscoCombatHelper.checkSequence( data, 1 ) ) { combo = i; DiscoCombatHelper.counter = 0; break; } } if ( combo >= 0 ) { StringBuilder buffer = new StringBuilder(); buffer.append( combo < FIRST_RAVE_COMBO ? "Disco" : "Rave" ); buffer.append( " combo: " ); buffer.append( COMBOS[ combo ][0] ); String message = buffer.toString(); RequestLogger.printLine( message ); RequestLogger.updateSessionLog( message ); } // Track successful Rave Steal usage if ( combo == RAVE_STEAL ) { // Rave Steal in the volcano island shouldn't count MonsterData monster = MonsterStatusTracker.getLastMonster(); String encounter = ""; if ( monster != null ) { encounter = monster.getName(); } if ( encounter.equalsIgnoreCase( "Breakdancing Raver" ) || encounter.equalsIgnoreCase( "Pop-and-Lock Raver" ) || encounter.equalsIgnoreCase( "Running Man" ) ) { } // You're getting tired of this same old song and dance. else if ( responseText.indexOf( "same old song and dance" ) != -1 ) { Preferences.setInteger( "_raveStealCount", 30 ); } else if ( responseText.indexOf( "You acquire an item" ) != -1 ) { Preferences.increment( "_raveStealCount" ); } } // If three different rave skills are used in sequence, // identify the rave combo if ( DiscoCombatHelper.counter == 3 ) { // Your savage beatdown seems to have knocked loose // some treasure. Sweet! // Your savage beatdown fails to knock loose any treasure. Lame! if ( responseText.indexOf( "Your savage beatdown" ) != -1 ) { DiscoCombatHelper.learnRaveCombo( RAVE_STEAL ); } // As your opponent groans in pain, you feel pretty // good about the extra dance practice you're // getting. You're starting to get tired of beating up // on this same dude. Why isn't he dead yet? else if ( responseText.indexOf( "extra dance practice" ) != -1 ) { DiscoCombatHelper.learnRaveCombo( RAVE_SUBSTATS ); } // Your dance routine leaves you feeling extra-focused // and in the zone. Ooh yeeaah. else if ( responseText.indexOf( "extra-focused and in the zone" ) != -1 ) { DiscoCombatHelper.learnRaveCombo( RAVE_CONCENTRATION ); } // Your dance routine leaves you feeling particularly // groovy and at one with the universe. It's a little // unsettling, but you soon get used to it. else if ( responseText.indexOf( "feeling particularly groovy" ) != -1 ) { DiscoCombatHelper.learnRaveCombo( RAVE_NIRVANA ); } } } private static final void learnRaveCombo( int combo ) { // Sanity check: we used three skills in a row if ( DiscoCombatHelper.counter != 3 ) { return; } int skill1 = DiscoCombatHelper.sequence[0]; int skill2 = DiscoCombatHelper.sequence[1]; int skill3 = DiscoCombatHelper.sequence[2]; // Sanity check: last three skills must all be different if ( skill1 == skill2 || skill1 == skill3 || skill2 == skill3 ) { return; } // Sanity check: last three skills must all be rave skills if ( skill1 < FIRST_RAVE_SKILL || skill1 > LAST_RAVE_SKILL || skill2 < FIRST_RAVE_SKILL || skill2 > LAST_RAVE_SKILL || skill3 < FIRST_RAVE_SKILL || skill3 > LAST_RAVE_SKILL ) { return; } // Clear sequence counter DiscoCombatHelper.counter = 0; // If we already know this combo, nothing to do if ( knownCombo[ combo ] ) { return; } // We have learned the combo! DiscoCombatHelper.learnRaveCombo( combo, skill1, skill2, skill3 ); // Update the random combo DiscoCombatHelper.checkCombo( RANDOM_RAVE ); } private static final void learnRaveCombo( int combo, int skill1, int skill2, int skill3 ) { knownCombo[ combo ] = true; // Generate the setting. String setting = "raveCombo" + String.valueOf( combo - FIRST_RAVE_COMBO + 1 ); String value = SKILLS[ skill1 ] + "," + SKILLS[ skill2 ] + "," + SKILLS[ skill3 ]; Preferences.setString( setting, value ); // Save the skills in the table int [][] data = COMBO_SKILLS[ combo ]; data[0][0] = skill1; data[1][0] = skill2; data[2][0] = skill3; StringBuilder buffer = new StringBuilder(); buffer.append( "You learned a new Rave Combo!" ); buffer.append( KoLConstants.LINE_BREAK ); buffer.append( SKILLS[ skill1 ] ); buffer.append( " + " ); buffer.append( SKILLS[ skill2 ] ); buffer.append( " + " ); buffer.append( SKILLS[ skill3 ] ); buffer.append( " -> " ); buffer.append( COMBOS[ combo ][0] ); String message = buffer.toString(); RequestLogger.printLine( message ); RequestLogger.updateSessionLog( message ); } private static final boolean checkSequence( final int[][] data, final int offset ) { // Compare the skill sequence (starting at offset) to a given // combo. for ( int i = 0; i < data.length; ++i ) { int skill = DiscoCombatHelper.sequence[ i + offset ]; int [] skills = data[ i ]; boolean found = false; for ( int j = 0; j < skills.length; ++j ) { if ( skill == skills[ j ] ) { found = true; break; } } if ( !found ) { return false; } } return true; } private static final StringBuffer generateTable() { StringBuffer buffer = new StringBuffer(); int combos = 0; buffer.append( "<table border=2 cols=5>" ); if ( DiscoCombatHelper.counter > 0 ) { buffer.append( "<caption>" ); for ( int i = 0; i < DiscoCombatHelper.counter; ++i ) { if ( i > 0 ) { buffer.append( ", " ); } int skill = DiscoCombatHelper.sequence[i]; buffer.append( SKILLS[ skill ] ); } buffer.append( "</caption>" ); } for ( int i = 0; i < NUM_COMBOS; ++i ) { if ( !knownCombo[ i ] ) { continue; } // Count this combo combos++; String [] combo = COMBOS[ i ]; buffer.append( "<tr>" ); // Combo name DiscoCombatHelper.addComboButton( buffer, combo[ 0 ], getCombo( i ) ); // Combo effect buffer.append( "<td>" ); buffer.append( combo[ 1 ] ); buffer.append( "</td>" ); int [][] data = COMBO_SKILLS[ i ]; for ( int j = 0; j < 3; ++j ) { if ( j < data.length ) { int [] skills = data[ j ]; boolean first = true; for ( int k = 0; k < skills.length; ++k ) { int skill = skills[ k ]; String name = SKILLS[ skill ]; if ( !KoLCharacter.hasSkill( name ) ) { continue; } if ( first ) { first = false; } else { buffer.append( "<br>" ); } // Add the button DiscoCombatHelper.addDiscoButton( buffer, skill, true ); } } else { buffer.append( "<td> </td>" ); } } buffer.append( "</tr>" ); } buffer.append( "</table>" ); // If no combos are known, no table. if ( combos == 0 ) { buffer.setLength( 0 ); } return buffer; } private static final void addComboButton( final StringBuffer buffer, final String name, int[] combo ) { buffer.append( "<form method=POST action=\"fight.php\"><td>" ); buffer.append( "<input type=hidden name=\"action\" value=\"macro\">" ); buffer.append( "<input type=hidden name=\"macrotext\" value=\"" ); int cost = 0; for ( int i = 0; i < combo.length; ++i ) { int skillId = combo[ i ]; cost += SkillDatabase.getMPConsumptionById( skillId ); buffer.append( "skill " ); buffer.append( skillId ); buffer.append( ";" ); } buffer.append( "\"><input onclick=\"return killforms(this);\" type=\"submit\" value=\"" ); buffer.append( name ); buffer.append( "\"" ); if ( DiscoCombatHelper.counter > 0 && DiscoCombatHelper.counter < 3 || cost > KoLCharacter.getCurrentMP() ) { buffer.append( " disabled" ); } buffer.append( "> </td></form>" ); } private static final void addDiscoButton( final StringBuffer buffer, final int skill, boolean isEnabled ) { String skillName = SKILLS[ skill ]; int skillId = SKILL_ID[ skill ]; String name = BUTTON_NAME[ skill ]; buffer.append( "<form method=POST action=\"fight.php\"><td>" ); buffer.append( "<input type=hidden name=\"action\" value=\"skill\">" ); buffer.append( "<input type=hidden name=\"whichskill\" value=\"" ); buffer.append( String.valueOf( skillId ) ); buffer.append( "\"><input onclick=\"return killforms(this);\" type=\"submit\" value=\"" ); buffer.append( name ); buffer.append( "\"" ); // Shouldn't be here if don't have skill, but just in case... if ( isEnabled ) { isEnabled &= KoLCharacter.hasSkill( skillName ); } // Make sure we have the MP to use the skill if ( isEnabled ) { isEnabled &= SkillDatabase.getMPConsumptionById( skillId ) <= KoLCharacter.getCurrentMP(); } if ( !isEnabled ) { buffer.append( " disabled" ); } buffer.append( "> </td></form>" ); } public static final void decorate( final StringBuffer buffer ) { // If you're not a Disco Bandit, nothing to do. if ( !DiscoCombatHelper.canCombo ) { return; } // If the fight is over, punt if ( FightRequest.getCurrentRound() == 0 ) { return; } // If you are in Birdform, uh-uh if ( KoLConstants.activeEffects.contains( FightRequest.BIRDFORM ) ) { return; } // If you are in Limitmode, no way if ( KoLCharacter.getLimitmode() != null ) { return; } // If you don't want the Disco Helper, you don't have to have it if ( !Preferences.getBoolean( "relayAddsDiscoHelper" ) ) { return; } int index = buffer.lastIndexOf( "</table></center></td>" ); if ( index != -1 ) { StringBuffer table = DiscoCombatHelper.generateTable(); table.insert( 0, "<tr>" ); table.append( "</tr>" ); buffer.insert( index, table ); } } }