/* Copyright (c) 2008-2010, developers of the Ascension Log Visualizer * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom * the Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ package com.googlecode.logVisualizer.logData.turn; import java.util.Collection; import java.util.Iterator; import java.util.SortedSet; import java.util.TreeSet; import com.googlecode.logVisualizer.logData.Item; import com.googlecode.logVisualizer.logData.MeatGain; import com.googlecode.logVisualizer.logData.Skill; import com.googlecode.logVisualizer.logData.Statgain; import com.googlecode.logVisualizer.logData.consumables.Consumable; import com.googlecode.logVisualizer.parser.UsefulPatterns; /** * An implementation for a turn interval. It can either consist of single turns * or simply a specified start and end turn. Note that the start turn is the * turn number of the last turn <b>before</b> this turn interval. * <p> * All methods in this class throw a {@link NullPointerException} if a null * object reference is passed in any parameter. * <p> * Note: This class has a natural ordering that is inconsistent with equals. */ public final class TurnInterval extends AbstractTurn implements Comparable<TurnInterval> { private int startTurn; private int endTurn; private SortedSet<SingleTurn> turns = new TreeSet<>(); private int successfulFreeRunaways; private String notes = UsefulPatterns.EMPTY_STRING; /** * If you use this constructor, please adhere to the standard set by * {@link #getStartTurn()} for the start turn number, namely that it is the * number of the last turn <b>before</b> this turn interval started. * <p> * Note that if the value of endTurn is smaller than the value of startTurn, * the ending turn of this interval will be set to {@code startTurn}. * * @param areaName * The name of the area of this turn interval to set. * @param startTurn * The start of this turn interval to set. * @param endTurn * The end of this turn interval to set. * @throws IllegalArgumentException * if startTurn is below 0; if endTurn is below 0 */ public TurnInterval(final String areaName, final int startTurn, final int endTurn) { super(areaName); if ((startTurn < 0) || (endTurn < 0)) { throw new IllegalArgumentException("Turn range below 0."); } this.startTurn = startTurn; this.endTurn = endTurn >= startTurn ? endTurn : startTurn; } /** * Constructs a turn interval with the given turn as a part of it. * * @param turn * Starting point of this turn interval to set. */ public TurnInterval(final SingleTurn turn) { super(turn.getAreaName()); final int tmpStartTurn = turn.getTurnNumber() - 1; this.startTurn = tmpStartTurn < 0 ? 0 : tmpStartTurn; this.endTurn = turn.getTurnNumber(); this.addTurnData(turn); this.turns.add(turn); } /** * Constructs a turn interval with the given turns as its content. * * @param turns * The single turns of this turn interval to set. * @throws IllegalArgumentException * if turns is empty; if areaName is not equal to the area name * of the turns inside the collection */ public TurnInterval(final SortedSet<SingleTurn> turns, final String areaName) { super(areaName); if (turns.isEmpty()) { throw new IllegalArgumentException( "Turns collection must not be empty."); } final Iterator<SingleTurn> index = turns.iterator(); SingleTurn turn = index.next(); if (!turn.getAreaName().equals(areaName)) { throw new IllegalArgumentException( "Area name parameter and area name of the turns inside the collcetion must be equal."); } this.addTurnData(turn); this.turns.add(turn); this.setStartEndInterval(); while (index.hasNext()) { turn = index.next(); if (!turn.getAreaName().equals(areaName)) { throw new IllegalArgumentException( "Area name parameter and area name of the turns inside the collcetion must be equal."); } this.addTurn(turn); } } /** * Helper method will set the start and end turn of this interval correctly. * Note that this method only works if the single turn collection is not * empty. */ private void setStartEndInterval() { this.startTurn = this.turns.first().getTurnNumber() - 1; this.endTurn = this.turns.last().getTurnNumber(); } /** * Checks the given turn on whether a runaway was done with the Navel Ring * equipped and increments the Navel Ring usage summary. */ private int countUnsuccessfulNavelRingUsages() { int tmp = 0; for (final SingleTurn turn : this.turns) { if (turn.isRanAwayOnThisTurn() && turn.isNavelRingEquipped()) { tmp++; } } return tmp; } /** * @param turns * The turns whose data will be added to this turn interval and * the last single turn of this interval if one is present. */ public void addTurnIntervalData(final TurnInterval turns) { if (!this.turns.isEmpty()) { this.turns.last().addTurnData(turns); } this.addTurnData(turns); this.addNotes(turns.getNotes()); this.successfulFreeRunaways += turns.getFreeRunaways() .getNumberOfSuccessfulRunaways(); } /** * Simply an alias for {@link #addStatGain(int, int, int)}. */ public void addStats(final int mus, final int myst, final int mox) { this.addStatGain(mus, myst, mox); } /** * These statgains will be added to this turn interval and the last single * turn of this interval, if one is present. * * @param mus * The muscle stat gain to add. * @param myst * The mysticality stat gain to add. * @param mox * The moxie stat gain to add. */ @Override public void addStatGain(final int mus, final int myst, final int mox) { if (!this.turns.isEmpty()) { this.turns.last().addStatGain(mus, myst, mox); } super.addStatGain(mus, myst, mox); } /** * These mp gains will be added to this turn interval and the last single * turn of this interval, if one is present. * * @param encounterMPGain * The encounter mp gain to add. This should not include starfish * mp gains or mp gains from resting. */ public void addEncounterMPGain(final int encounterMPGain) { if (!this.turns.isEmpty()) { this.turns.last().getMPGain().addEncounterMPGain(encounterMPGain); } this.getMPGain().addEncounterMPGain(encounterMPGain); } /** * These mp gains will be added to this turn interval and the last single * turn of this interval, if one is present. * * @param starfishMPGain * The starfish mp gain to add. */ public void addStarfishMPGain(final int starfishMPGain) { if (!this.turns.isEmpty()) { this.turns.last().getMPGain().addStarfishMPGain(starfishMPGain); } this.getMPGain().addStarfishMPGain(starfishMPGain); } /** * These mp gains will be added to this turn interval and the last single * turn of this interval, if one is present. * * @param restingMPGain * The resting mp gain to add. */ public void addRestingMPGain(final int restingMPGain) { if (!this.turns.isEmpty()) { this.turns.last().getMPGain().addRestingMPGain(restingMPGain); } this.getMPGain().addRestingMPGain(restingMPGain); } /** * These mp gains will be added to this turn interval and the last single * turn of this interval, if one is present. * * @param outOfEncounterMPGain * The out-of-encounter mp gain to add. */ public void addOutOfEncounterMPGain(final int outOfEncounterMPGain) { if (!this.turns.isEmpty()) { this.turns.last().getMPGain() .addOutOfEncounterMPGain(outOfEncounterMPGain); } this.getMPGain().addOutOfEncounterMPGain(outOfEncounterMPGain); } /** * These mp gains will be added to this turn interval and the last single * turn of this interval, if one is present. * * @param consumableMPGain * The consumable mp gain to add. */ public void addConsumableMPGain(final int consumableMPGain) { if (!this.turns.isEmpty()) { this.turns.last().getMPGain().addConsumableMPGain(consumableMPGain); } this.getMPGain().addConsumableMPGain(consumableMPGain); } /** * The meat data will be added to this turn interval and the last single * turn of this interval, if one is present. * * @param meat * The meat data to add. */ @Override public void addMeat(final MeatGain meat) { if (!this.turns.isEmpty()) { this.turns.last().addMeat(meat); } super.addMeat(meat); } /** * The meat gained will be added to this turn interval and the last single * turn of this interval, if one is present. * * @param encounterMeatGain * The meat gain from inside the encounter to add. */ @Override public void addEncounterMeatGain(final int encounterMeatGain) { if (!this.turns.isEmpty()) { this.turns.last().addEncounterMeatGain(encounterMeatGain); } super.addEncounterMeatGain(encounterMeatGain); } /** * The meat gained will be added to this turn interval and the last single * turn of this interval, if one is present. * * @param otherMeatGain * The meat gain from outside the encounter to add. */ @Override public void addOtherMeatGain(final int otherMeatGain) { if (!this.turns.isEmpty()) { this.turns.last().addOtherMeatGain(otherMeatGain); } super.addOtherMeatGain(otherMeatGain); } /** * The meat spent will be added to this turn interval and the last single * turn of this interval, if one is present. * * @param meatSpent * The meat spent to add. */ @Override public void addMeatSpent(final int meatSpent) { if (!this.turns.isEmpty()) { this.turns.last().addMeatSpent(meatSpent); } super.addMeatSpent(meatSpent); } /** * The item will be added to this turn interval and the last single turn of * this interval, if one is present. * * @param droppedItem * The item to add. */ @Override public void addDroppedItem(final Item droppedItem) { if (!this.turns.isEmpty()) { this.turns.last().addDroppedItem(droppedItem); } super.addDroppedItem(droppedItem); } /** * The skill will be added to this turn interval and the last single turn of * this interval, if one is present. * * @param skill * The skill to add. */ @Override public void addSkillCast(final Skill skill) { if (!this.turns.isEmpty()) { this.turns.last().addSkillCast(skill); } super.addSkillCast(skill); } /** * The consumable will be added to this turn interval and the last single * turn of this interval, if one is present. * * @param consumable * The consumable to add. */ @Override public void addConsumableUsed(final Consumable consumable) { if (!this.turns.isEmpty()) { this.turns.last().addConsumableUsed(consumable); } super.addConsumableUsed(consumable); } /** * {@inheritDoc} */ @Override protected void addTurnData(final AbstractTurn turn) { if (turn == null) { throw new NullPointerException("Turn must not be null."); } final Statgain turnStats = turn.getStatGain(); super.addMeat(turn.getMeat()); super.addStatGain(turnStats.mus, turnStats.myst, turnStats.mox); this.getMPGain().addMPGains(turn.getMPGain()); for (final Item i : turn.getDroppedItems()) { super.addDroppedItem(i); } for (final Skill s : turn.getSkillsCast()) { super.addSkillCast(s); } for (final Consumable c : turn.getConsumablesUsed()) { super.addConsumableUsed(c); } } /** * Will return the turn number of the last turn before this turn interval * starts. Thus, this method is equal to * {@code getTurns().get(0).getTurnNumber-1}. The reasoning behind this is * that a turn interval consisting of one single turn is the interval from * {@code turn.getTurnNumber()-1} to {@code turn.getTurnNumber()}. * * @return The start of this turn interval. */ public int getStartTurn() { return this.startTurn; } /** * @return The last turn of this turn interval. */ public int getEndTurn() { return this.endTurn; } /** * @return The amount of turns in this turn interval. */ public int getTotalTurns() { return this.endTurn - this.startTurn; } /** * Adds the given turn to this turn interval. * <p> * In case there is already a turn present with the same turn number, that * turn will replaced with the given turn and the turn data of the replaced * turn will be added to the turn before it or the given turn if the * replaced turn is the first in the interval. * * @param turn * The turn to add. * @throws IllegalStateException * if this class wasn't initialised with single turns (which is * equal to a call to getTurns().isEmpty() returning true) * @throws IllegalArgumentException * if area name of the turn interval is not equal to the area * name of the given turn */ public void addTurn(final SingleTurn turn) { if (this.turns.isEmpty()) { throw new IllegalStateException( "Adding single turns to an interval that wasn't initialised with single turns isn't supported."); } if (!this.getAreaName().equals(turn.getAreaName())) { throw new IllegalArgumentException( "Area name of the turn interval is not the same as the area name of the given turn."); } this.addTurnData(turn); // If there is only one turn present in the interval, check whether // there is a big turn number difference between the turn that's // supposed to be added and the turn in the interval. If that is the // case, treat it as an error of a mafia log. Sometimes after // ascensions, mafia doesn't check the turn counts and uses wrong // numbers for the first few turns. Not all these errors will be catched // by this check, but at least some should be. if (this.turns.size() == 1) { final SingleTurn onlyTurn = this.turns.first(); if ((turn.getTurnNumber() - onlyTurn.getTurnNumber()) < -2) { final SingleTurn tmp = new SingleTurn(onlyTurn.getAreaName(), onlyTurn.getEncounterName(), turn.getTurnNumber() - 1, onlyTurn.getUsedEquipment(), onlyTurn.getUsedFamiliar()); tmp.addSingleTurnData(onlyTurn); this.turns.remove(onlyTurn); this.turns.add(tmp); } } if (this.turns.contains(turn)) { // Add all the data from the turn which should be removed to the // turn that comes before it, or if there is none, to the new turn // so the data is not lost. final SortedSet<SingleTurn> tmp = this.turns.headSet(turn); final SingleTurn equalTurn = this.turns.tailSet(turn).first(); if (!tmp.isEmpty()) { tmp.last().addSingleTurnData(equalTurn); } else { turn.addSingleTurnData(equalTurn); } // If the skipped turn was a runaway and the Navel Ring was // equipped, it means that it was a successful usage of the Navel // Ring. if (equalTurn.isRanAwayOnThisTurn() && equalTurn.isNavelRingEquipped()) { this.successfulFreeRunaways++; } this.turns.remove(equalTurn); } this.turns.add(turn); this.setStartEndInterval(); } /** * Removes the first turn of this interval and adds all the data of the turn * to the following one. If no other turn exists, the data will simply be * lost. * * @return The removed turn. * @throws IllegalStateException * if this class wasn't initialised with single turns (which is * equal to a call to getTurns().isEmpty() returning true) */ public SingleTurn removeFirstTurn() { if (this.turns.isEmpty()) { throw new IllegalStateException( "Removing single turns from an interval that wasn't initialised with single turns isn't supported."); } final SingleTurn turn = this.turns.first(); this.turns.remove(turn); if (!this.turns.isEmpty()) { this.turns.first().addSingleTurnData(turn); this.setStartEndInterval(); } return turn; } /** * Removes the last turn of this interval and adds all the data of the turn * to the one coming before it. If no other turn exists, the data will * simply be lost. * * @return The removed turn. * @throws IllegalStateException * if this class wasn't initialised with single turns (which is * equal to a call to getTurns().isEmpty() returning true) */ public SingleTurn removeLastTurn() { if (this.turns.isEmpty()) { throw new IllegalStateException( "Removing single turns from an interval that wasn't initialised with single turns isn't supported."); } final SingleTurn turn = this.turns.last(); this.turns.remove(turn); if (!this.turns.isEmpty()) { this.turns.last().addSingleTurnData(turn); this.setStartEndInterval(); } return turn; } /** * @param turns * The single turns to set. * @throws IllegalArgumentException * if turns is empty */ public void setTurns(final Collection<SingleTurn> turns) { if (turns.isEmpty()) { throw new IllegalArgumentException( "Turn collection must not be empty."); } this.turns = new TreeSet<>(turns); this.setStartEndInterval(); this.clearAllTurnDataCollections(); this.setStatGain(0, 0, 0); this.setMeat(new MeatGain()); for (final SingleTurn turn : turns) { this.addTurnData(turn); } } /** * Returns the collection of single turns spent during this turn interval. * Please note that the set returned by this method is directly backed by * the internal collection of this class and thus, great care should be * taken when handling its elements. * * @return The single turns of this turn interval. Can be empty, if no turns * have been set. */ public SortedSet<SingleTurn> getTurns() { return this.turns; } /** * @param successfulNavelRingUsages * The number of successful Navel Ring usages to set. */ public void setSuccessfulFreeRunaways(final int successfulFreeRunaways) { this.successfulFreeRunaways = successfulFreeRunaways; } /** * @param successfulNavelRingUsages * The number of successful Navel Ring usages to increment the * already existing value by.. */ public void incrementSuccessfulFreeRunaways(final int successfulFreeRunaways) { this.successfulFreeRunaways += successfulFreeRunaways; } /** * @return The Navel Ring usage of this turn interval. */ public FreeRunaways getFreeRunaways() { return new FreeRunaways(this.successfulFreeRunaways + this.countUnsuccessfulNavelRingUsages(), this.successfulFreeRunaways); } /** * @param notes * The notes tagged to this turn interval to set. */ public void setNotes(final String notes) { if (notes == null) { throw new NullPointerException("notes must not be null."); } this.notes = notes; } /** * Adds the given notes to this turn interval. The already existing notes * and the ones added will be divided by a line break ({@code"\n"}). * * @param notes * The notes tagged to this turn interval to set. */ public void addNotes(final String notes) { if (notes == null) { throw new NullPointerException("notes must not be null."); } if (notes.length() > 0) { final StringBuilder str = new StringBuilder(this.notes.length() + notes.length() + 1); if (this.notes.length() > 0) { str.append(this.notes); str.append("\n"); } str.append(notes); this.notes = str.toString(); } } /** * @return The notes tagged to this turn interval. */ public String getNotes() { return this.notes; } @Override public String toString() { final StringBuilder str = new StringBuilder(60); str.append(UsefulPatterns.SQUARE_BRACKET_OPEN); if (this.getTotalTurns() > 1) { str.append(this.startTurn + 1); str.append(UsefulPatterns.MINUS); } str.append(this.endTurn); str.append(UsefulPatterns.SQUARE_BRACKET_CLOSE); str.append(UsefulPatterns.WHITE_SPACE); str.append(this.getAreaName()); str.append(UsefulPatterns.WHITE_SPACE); str.append(this.getStatGain().toString()); return str.toString(); } /** * @return The difference between the start turns of this turn interval and * the given turn interval. */ @Override public int compareTo(final TurnInterval turns) { return this.startTurn - turns.getStartTurn(); } @Override public boolean equals(final Object o) { if (o != null) { if (o instanceof TurnInterval) { final boolean isSameTurnRange = (this.startTurn == ((TurnInterval) o) .getStartTurn()) && (this.endTurn == ((TurnInterval) o).getEndTurn()); return isSameTurnRange && super.equals(o) && this.turns.equals(((TurnInterval) o).getTurns()); } } return false; } @Override public int hashCode() { int result = 11; result = (31 * result) + this.startTurn; result = (31 * result) + this.endTurn; result = (31 * result) + super.hashCode(); result = (31 * result) + this.turns.hashCode(); return result; } /** * This immutable class is a representation of the runaway usage of the * Navel Ring. */ public static final class FreeRunaways { private static final String SLASH = "/"; private static final String FREE_RETREATS_STRING = "free retreats"; private final int numberOfAttemptedRunaways; private final int numberOfSuccessfulRunaways; /** * Creates a new FreeRunaways instance with the given number of runaways * and successful runaways. * * @throws IllegalArgumentException * if numberOfSuccessfulUsages is greater than * numberOfAttemptedUsages */ public FreeRunaways(final int numberOfAttemptedRunaways, final int numberOfSuccessfulRunaways) { if (numberOfSuccessfulRunaways > numberOfAttemptedRunaways) { throw new IllegalArgumentException( "Number of successful usages cannot be below number of usages."); } this.numberOfAttemptedRunaways = numberOfAttemptedRunaways; this.numberOfSuccessfulRunaways = numberOfSuccessfulRunaways; } /** * @return The number of attempted runaway usages of the Navel Ring. */ public int getNumberOfAttemptedRunaways() { return this.numberOfAttemptedRunaways; } /** * @return The number of successful runaway usages of the Navel Ring. */ public int getNumberOfSuccessfulRunaways() { return this.numberOfSuccessfulRunaways; } @Override public String toString() { final StringBuilder str = new StringBuilder(25); str.append(this.numberOfSuccessfulRunaways); str.append(UsefulPatterns.WHITE_SPACE); str.append(FreeRunaways.SLASH); str.append(UsefulPatterns.WHITE_SPACE); str.append(this.numberOfAttemptedRunaways); str.append(UsefulPatterns.WHITE_SPACE); str.append(FreeRunaways.FREE_RETREATS_STRING); return str.toString(); } @Override public boolean equals(final Object o) { if (super.equals(o) && (o instanceof FreeRunaways)) { return ((FreeRunaways) o).getNumberOfSuccessfulRunaways() == this.numberOfSuccessfulRunaways; } return false; } @Override public int hashCode() { int result = 687; result = (31 * result) + super.hashCode(); result = (31 * result) + this.numberOfSuccessfulRunaways; return result; } } }