diff --git a/src/main/java/matsyir/pvpperformancetracker/PvpPerformanceTrackerPlugin.java b/src/main/java/matsyir/pvpperformancetracker/PvpPerformanceTrackerPlugin.java index 1852d2c..1e3a76f 100644 --- a/src/main/java/matsyir/pvpperformancetracker/PvpPerformanceTrackerPlugin.java +++ b/src/main/java/matsyir/pvpperformancetracker/PvpPerformanceTrackerPlugin.java @@ -81,14 +81,8 @@ import net.runelite.api.Prayer; import net.runelite.api.Skill; import net.runelite.api.SpriteID; -import net.runelite.api.events.AnimationChanged; -import net.runelite.api.events.FakeXpDrop; -import net.runelite.api.events.GameTick; -import net.runelite.api.events.GameStateChanged; -import net.runelite.api.events.HitsplatApplied; -import net.runelite.api.events.InteractingChanged; - -import net.runelite.api.events.StatChanged; +import net.runelite.api.events.*; + import net.runelite.client.RuneLite; import net.runelite.client.callback.ClientThread; import net.runelite.client.chat.ChatMessageManager; @@ -118,7 +112,7 @@ @Slf4j @PluginDescriptor( - name = "PvP Performance Tracker" + name = "PvP Performance Tracker" ) public class PvpPerformanceTrackerPlugin extends Plugin { @@ -228,11 +222,11 @@ protected void startUp() throws Exception fightHistory = new ArrayList<>(); GSON = injectedGson.newBuilder() - .excludeFieldsWithoutExposeAnnotation() - .registerTypeAdapter(Double.class, (JsonSerializer) (value, theType, context) -> - value.isNaN() ? new JsonPrimitive(0) // Convert NaN to zero, otherwise, return as BigDecimal with scale of 3. - : new JsonPrimitive(BigDecimal.valueOf(value).setScale(3, RoundingMode.HALF_UP)) - ).create(); + .excludeFieldsWithoutExposeAnnotation() + .registerTypeAdapter(Double.class, (JsonSerializer) (value, theType, context) -> + value.isNaN() ? new JsonPrimitive(0) // Convert NaN to zero, otherwise, return as BigDecimal with scale of 3. + : new JsonPrimitive(BigDecimal.valueOf(value).setScale(3, RoundingMode.HALF_UP)) + ).create(); if (!config.pluginVersion().equals(PLUGIN_VERSION)) { @@ -243,17 +237,17 @@ protected void startUp() throws Exception final BufferedImage icon = ImageUtil.getResourceStreamFromClass(getClass(), "/skull_red.png"); PLUGIN_ICON = new ImageIcon(icon).getImage(); navButton = NavigationButton.builder() - .tooltip("PvP Fight History") - .icon(icon) - .priority(6) - .panel(panel) - .build(); + .tooltip("PvP Fight History") + .icon(icon) + .priority(6) + .panel(panel) + .build(); importFightHistoryData(); // add the panel's nav button depending on config if (config.showFightHistoryPanel() && - (!config.restrictToLms() || (client.getGameState() == GameState.LOGGED_IN && isAtLMS()))) + (!config.restrictToLms() || (client.getGameState() == GameState.LOGGED_IN && isAtLMS()))) { navButtonShown = true; clientToolbar.addNavigation(navButton); @@ -265,13 +259,13 @@ protected void startUp() throws Exception // prepare default N/A or None symbol for eventual use. clientThread.invokeLater(() -> DEFAULT_NONE_SYMBOL = itemManager.getImage(20594)); - + // Explicitly rebuild panel after all setup and import. - SwingUtilities.invokeLater(() -> { - if (panel != null) { - panel.rebuild(); - } - }); + SwingUtilities.invokeLater(() -> { + if (panel != null) { + panel.rebuild(); + } + }); } @Override @@ -295,7 +289,7 @@ public void onConfigChanged(ConfigChanged event) case "restrictToLms": boolean isAtLms = isAtLMS(); if (!navButtonShown && config.showFightHistoryPanel() && - (!config.restrictToLms() || isAtLms)) + (!config.restrictToLms() || isAtLms)) { SwingUtilities.invokeLater(() -> clientToolbar.addNavigation(navButton)); navButtonShown = true; @@ -340,7 +334,7 @@ else if (navButtonShown && (!config.showFightHistoryPanel() || (config.restrictT case "robeHitFilter": recalculateAllRobeHits(true); break; - // potential future code for level presets/dynamic config if RL ever supports it. + // potential future code for level presets/dynamic config if RL ever supports it. // case "attackLevel": // case "strengthLevel": // case "defenceLevel": @@ -389,8 +383,8 @@ public void onInteractingChanged(InteractingChanged event) // if the client player already has a valid opponent AND the fight has started, // or the event source/target aren't players, skip any processing. if ((hasOpponent() && currentFight.fightStarted()) - || !(event.getSource() instanceof Player) - || !(event.getTarget() instanceof Player)) + || !(event.getSource() instanceof Player) + || !(event.getTarget() instanceof Player)) { return; } @@ -414,7 +408,7 @@ else if (event.getTarget().equals(client.getLocalPlayer())) // start a new fight with the new found opponent, if a new one. if (!hasOpponent() || !currentFight.getOpponent().getName().equals(opponent.getName())) { - currentFight = new FightPerformance(client.getLocalPlayer(), (Player)opponent); + currentFight = new FightPerformance(client.getLocalPlayer(), (Player)opponent, hiscoreManager); overlay.setFight(currentFight); hitsplatBuffer.clear(); incomingHitsplatsBuffer.clear(); @@ -625,24 +619,24 @@ public void onGameTick(GameTick event) if (currentFight.getOpponent() != null) { totalExpectedAttackHits += currentFight.getOpponent().getPendingAttacks().stream() - .filter(e -> !e.isKoChanceCalculated() && e.isFullEntry() && !e.isSplash() && (tickToProcess - e.getTick() <= 5)) // Check if attack could land now - .mapToInt(FightLogEntry::getExpectedHits) - .sum(); + .filter(e -> !e.isKoChanceCalculated() && e.isFullEntry() && !e.isSplash() && (tickToProcess - e.getTick() <= 5)) // Check if attack could land now + .mapToInt(FightLogEntry::getExpectedHits) + .sum(); } // Sum expected hits from competitor's pending attacks targeting opponent if (currentFight.getCompetitor() != null) { totalExpectedAttackHits += currentFight.getCompetitor().getPendingAttacks().stream() - .filter(e -> !e.isKoChanceCalculated() && e.isFullEntry() && !e.isSplash() && (tickToProcess - e.getTick() <= 5)) // Check if attack could land now - .mapToInt(FightLogEntry::getExpectedHits) - .sum(); + .filter(e -> !e.isKoChanceCalculated() && e.isFullEntry() && !e.isSplash() && (tickToProcess - e.getTick() <= 5)) // Check if attack could land now + .mapToInt(FightLogEntry::getExpectedHits) + .sum(); } // 2. Compare observed vs expected if (hitsplatsToProcess.size() > totalExpectedAttackHits) { log.debug("Tick {}: Observed hits ({}) > Expected attack hits ({}). Checking for special hits...", - tickToProcess, hitsplatsToProcess.size(), totalExpectedAttackHits); + tickToProcess, hitsplatsToProcess.size(), totalExpectedAttackHits); boolean removedHitInIteration; int safetyBreakCounter = 0; @@ -690,7 +684,7 @@ else if (target == opponentActor) if (hitAmount == expectedVengeance) { log.debug("Tick {}: Found potential Vengeance hit ({} damage) on {} based on {} incoming damage on {}", - tickToProcess, hitAmount, target.getName(), incomingDamage, otherPlayer.getName()); + tickToProcess, hitAmount, target.getName(), incomingDamage, otherPlayer.getName()); isCandidate = true; break; // Found a reason, no need to check other incoming hits for this potentialSpecialHit } @@ -700,7 +694,7 @@ else if (target == opponentActor) if (hitAmount == expectedRecoil) { log.debug("Tick {}: Found potential Recoil hit ({} damage) on {} based on {} incoming damage on {}", - tickToProcess, hitAmount, target.getName(), incomingDamage, otherPlayer.getName()); + tickToProcess, hitAmount, target.getName(), incomingDamage, otherPlayer.getName()); isCandidate = true; break; } @@ -747,7 +741,7 @@ else if (target == opponentActor) // Group hitsplats by the actor receiving them (remaining hitsplats after special removal) final List finalHitsplatsToProcess = hitsplatsToProcess; // Create effectively final list Map> hitsByActor = finalHitsplatsToProcess.stream() - .collect(Collectors.groupingBy((HitsplatInfo info) -> info.getEvent().getActor())); + .collect(Collectors.groupingBy((HitsplatInfo info) -> info.getEvent().getActor())); List processedEntriesThisTick = new ArrayList<>(); @@ -765,19 +759,19 @@ else if (target == opponentActor) maxHpToUse = CONFIG.opponentHitpointsLevel(); // Hiscores lookup should only happen if not in LMS - if (opponent instanceof Player && opponent.getName() != null) - { - final HiscoreResult hiscoreResult = hiscoreManager.lookupAsync(opponent.getName(), hiscoreEndpoint); - if (hiscoreResult != null) + if (opponent instanceof Player && opponent.getName() != null) { - final int hp = hiscoreResult.getSkill(HiscoreSkill.HITPOINTS).getLevel(); - if (hp > 0) + final HiscoreResult hiscoreResult = hiscoreManager.lookupAsync(opponent.getName(), hiscoreEndpoint); + if (hiscoreResult != null) { - maxHpToUse = hp; // Use Hiscores HP if available + final int hp = hiscoreResult.getSkill(HiscoreSkill.HITPOINTS).getLevel(); + if (hp > 0) + { + maxHpToUse = hp; // Use Hiscores HP if available + } } } } - } // Determine attacker String actorName = ((Player) opponent).getName(); @@ -787,20 +781,20 @@ else if (target == opponentActor) attacker = currentFight.getCompetitor(); } else if (actorName.equals(currentFight.getCompetitor().getName())) - { + { attacker = currentFight.getOpponent(); - } - else - { + } + else + { return; } // Get all potentially relevant, unprocessed entries sorted by animation tick List candidateEntries = attacker.getPendingAttacks().stream() - .filter(e -> !e.isKoChanceCalculated() && e.isFullEntry() && !e.isSplash()) - .filter(e -> (client.getTickCount() - e.getTick()) <= 5) - .sorted(Comparator.comparingInt(FightLogEntry::getTick)) - .collect(Collectors.toList()); + .filter(e -> !e.isKoChanceCalculated() && e.isFullEntry() && !e.isSplash()) + .filter(e -> (client.getTickCount() - e.getTick()) <= 5) + .sorted(Comparator.comparingInt(FightLogEntry::getTick)) + .collect(Collectors.toList()); List gmaulsMatchedThisTick = new ArrayList<>(); int totalGmaulHitsMatchedThisTick = 0; @@ -924,14 +918,14 @@ else if (actorName.equals(currentFight.getCompetitor().getName())) { // Group processed entries by the tick they landed and the attacker Map>> groupedByTickAndAttacker = processedEntriesThisTick.stream() - .filter(e -> e.getHitsplatTick() >= 0) - .collect(Collectors.groupingBy( - FightLogEntry::getHitsplatTick, - Collectors.groupingBy( - FightLogEntry::getAttackerName, - Collectors.toList() - ) - )); + .filter(e -> e.getHitsplatTick() >= 0) + .collect(Collectors.groupingBy( + FightLogEntry::getHitsplatTick, + Collectors.groupingBy( + FightLogEntry::getAttackerName, + Collectors.toList() + ) + )); groupedByTickAndAttacker.forEach((tick, attackerMap) -> { attackerMap.forEach((attackerName, entries) -> { @@ -955,8 +949,8 @@ else if (actorName.equals(currentFight.getCompetitor().getName())) // Calculate total damage for the sequence int totalDamageInSequence = entries.stream() - .mapToInt((FightLogEntry e) -> e.getActualDamageSum() != null ? e.getActualDamageSum() : 0) - .sum(); + .mapToInt((FightLogEntry e) -> e.getActualDamageSum() != null ? e.getActualDamageSum() : 0) + .sum(); // Calculate HP Before the entire sequence hpBeforeSequence = hpAfterSequence + totalDamageInSequence; @@ -980,8 +974,8 @@ else if (actorName.equals(currentFight.getCompetitor().getName())) entry.setDisplayHpAfter(hpAfterCurrent); Double koChanceCurrent = (hpBeforeCurrent != null) - ? PvpPerformanceTrackerUtils.calculateKoChance(entry.getAccuracy(), entry.getMinHit(), entry.getMaxHit(), hpBeforeCurrent) - : null; + ? PvpPerformanceTrackerUtils.calculateKoChance(entry.getAccuracy(), entry.getMinHit(), entry.getMaxHit(), hpBeforeCurrent) + : null; entry.setDisplayKoChance(koChanceCurrent); entry.setKoChance(koChanceCurrent); @@ -991,7 +985,7 @@ else if (actorName.equals(currentFight.getCompetitor().getName())) // Update HP for the next iteration currentHp = hpAfterCurrent; - } + } }); }); } @@ -1090,7 +1084,7 @@ private void updateFrom1_5_5to1_5_6() // read the old saved fights from the file into an array, and add them as an updated // fight to the fightHistory list. Arrays.asList(GSON.fromJson(new FileReader(fightHistoryData), FightPerformance__1_5_5[].class)) - .forEach((oldFight) -> fightHistory.add(new FightPerformance(oldFight))); + .forEach((oldFight) -> fightHistory.add(new FightPerformance(oldFight))); // now that the fights were deserialized and updated to the newest version, simply save them. // afterwards, they will be re-loaded normally. Bit inefficient but not a big deal @@ -1152,10 +1146,10 @@ void addToFightHistory(FightPerformance fight) } catch (Exception e) { - log.warn("Error calculating robe hits for new fight ({} vs {}): {}", - fight.getCompetitor() != null ? fight.getCompetitor().getName() : "N/A", - fight.getOpponent() != null ? fight.getOpponent().getName() : "N/A", - e.getMessage()); + log.warn("Error calculating robe hits for new fight ({} vs {}): {}", + fight.getCompetitor() != null ? fight.getCompetitor().getName() : "N/A", + fight.getOpponent() != null ? fight.getOpponent().getName() : "N/A", + e.getMessage()); } // remove fights as necessary to respect the fightHistoryLimit. @@ -1221,7 +1215,7 @@ void importFightHistoryData() // read the saved fights from the file List savedFights = Arrays.asList( - GSON.fromJson(new FileReader(fightHistoryData), FightPerformance[].class)); + GSON.fromJson(new FileReader(fightHistoryData), FightPerformance[].class)); fightHistory.clear(); importFights(savedFights); @@ -1262,15 +1256,15 @@ public void initializeImportedFight(FightPerformance f) { // check for nulls in case the data was corrupted and entries are corrupted. if (f.getCompetitor() == null || f.getOpponent() == null || - f.getCompetitor().getFightLogEntries() == null || f.getOpponent().getFightLogEntries() == null) + f.getCompetitor().getFightLogEntries() == null || f.getOpponent().getFightLogEntries() == null) { return; } f.getCompetitor().getFightLogEntries().forEach((FightLogEntry l) -> - l.attackerName = f.getCompetitor().getName()); + l.attackerName = f.getCompetitor().getName()); f.getOpponent().getFightLogEntries().forEach((FightLogEntry l) -> - l.attackerName = f.getOpponent().getName()); + l.attackerName = f.getOpponent().getName()); } // process and add a list of deserialized json fights to the currently loaded fights @@ -1329,15 +1323,29 @@ public boolean isAtLMS() return false; } + public boolean isAtArena() + { + boolean atArena = false; + int world = client.getWorld(); + switch(world) { + case 570: + case 578: + case 558: + atArena = true; + break; + } + return atArena; + } + // Send a message to the chat. Send them messages to the trade chat since it is uncommonly // used while fighting, but game, public, private, and clan chat all have their uses. public void sendTradeChatMessage(String chatMessage) { chatMessageManager - .queue(QueuedMessage.builder() - .type(ChatMessageType.TRADE) - .runeLiteFormattedMessage(chatMessage) - .build()); + .queue(QueuedMessage.builder() + .type(ChatMessageType.TRADE) + .runeLiteFormattedMessage(chatMessage) + .build()); } // create a simple confirmation modal, using a custom dialog so it can be always @@ -1379,11 +1387,11 @@ public void exportFight(FightPerformance fight) boolean success = false; String confirmMessage; if (fight.getCompetitor() != null && fight.getCompetitor().getName() != null && - fight.getOpponent() != null && fight.getOpponent().getName() != null) + fight.getOpponent() != null && fight.getOpponent().getName() != null) { success = true; confirmMessage = "Fight data of " + fight.getCompetitor().getName() + " vs " + - fight.getOpponent().getName() + " was copied to the clipboard."; + fight.getOpponent().getName() + " was copied to the clipboard."; } else { @@ -1398,11 +1406,11 @@ public int currentlyUsedOffensivePray() { return client.isPrayerActive(Prayer.PIETY) ? SpriteID.PRAYER_PIETY : client.isPrayerActive(Prayer.ULTIMATE_STRENGTH) ? SpriteID.PRAYER_ULTIMATE_STRENGTH : - client.isPrayerActive(Prayer.RIGOUR) ? SpriteID.PRAYER_RIGOUR : - client.isPrayerActive(Prayer.EAGLE_EYE) ? SpriteID.PRAYER_EAGLE_EYE : - client.isPrayerActive(Prayer.AUGURY) ? SpriteID.PRAYER_AUGURY : - client.isPrayerActive(Prayer.MYSTIC_MIGHT) ? SpriteID.PRAYER_MYSTIC_MIGHT : - 0; + client.isPrayerActive(Prayer.RIGOUR) ? SpriteID.PRAYER_RIGOUR : + client.isPrayerActive(Prayer.EAGLE_EYE) ? SpriteID.PRAYER_EAGLE_EYE : + client.isPrayerActive(Prayer.AUGURY) ? SpriteID.PRAYER_AUGURY : + client.isPrayerActive(Prayer.MYSTIC_MIGHT) ? SpriteID.PRAYER_MYSTIC_MIGHT : + 0; } public void addSpriteToLabelIfValid(JLabel label, int spriteId, Runnable swingCallback) diff --git a/src/main/java/matsyir/pvpperformancetracker/controllers/FightPerformance.java b/src/main/java/matsyir/pvpperformancetracker/controllers/FightPerformance.java index 8b9f1a9..31bbb52 100644 --- a/src/main/java/matsyir/pvpperformancetracker/controllers/FightPerformance.java +++ b/src/main/java/matsyir/pvpperformancetracker/controllers/FightPerformance.java @@ -34,6 +34,7 @@ import java.util.Arrays; import java.util.Objects; import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import static matsyir.pvpperformancetracker.PvpPerformanceTrackerPlugin.CONFIG; @@ -45,10 +46,13 @@ import matsyir.pvpperformancetracker.models.FightType; import matsyir.pvpperformancetracker.models.oldVersions.FightPerformance__1_5_5; import net.runelite.api.AnimationID; +import net.runelite.api.Client; import net.runelite.api.Player; import net.runelite.api.Skill; import matsyir.pvpperformancetracker.PvpPerformanceTrackerConfig; import net.runelite.api.kit.KitType; +import net.runelite.client.hiscore.HiscoreManager; + import static matsyir.pvpperformancetracker.utils.PvpPerformanceTrackerUtils.fixItemId; import static matsyir.pvpperformancetracker.controllers.PvpDamageCalc.RANGE_DEF; @@ -59,9 +63,9 @@ public class FightPerformance implements Comparable { private static final int[] DEATH_ANIMATIONS = { - AnimationID.DEATH, // Default - 10629, // League IV - 11902, // League V + AnimationID.DEATH, // Default + 10629, // League IV + 11902, // League V }; // Delay to assume a fight is over. May seem long, but sometimes people barrage & // stand under for a while to eat. Fights will automatically end when either competitor dies. @@ -107,6 +111,13 @@ public class FightPerformance implements Comparable private transient double competitorSurvivalProb = 1.0; private transient double opponentSurvivalProb = 1.0; + @Getter + private CombatLevels playersStats = new CombatLevels(PLUGIN.getClient()); + + @Getter + @Setter + private CombatLevels opponentsStats = CombatLevels.getConfigLevels(); + // shouldn't be used, just here so we can make a subclass, weird java thing public FightPerformance() { @@ -114,15 +125,25 @@ public FightPerformance() } // constructor which initializes a fight from the 2 Players, starting stats at 0. Regular use constructor. - public FightPerformance(Player competitor, Player opponent) + public FightPerformance(Player competitor, Player opponent, HiscoreManager hiscoreManager) { int defLvl = PLUGIN.getClient().getBoostedSkillLevel(Skill.DEFENCE); // determine fight type based on being at LMS areas & use def level to check for LMS builds. - this.fightType = !PLUGIN.isAtLMS() ? FightType.NORMAL : - defLvl <= FightType.LMS_1DEF.getCombatLevelsForType().def ? FightType.LMS_1DEF : - defLvl <= FightType.LMS_ZERK.getCombatLevelsForType().def ? FightType.LMS_ZERK : - FightType.LMS_MAXMED; + boolean fightIsAtLMS = PLUGIN.isAtLMS(); + boolean fightIsAtArena = PLUGIN.isAtArena(); + this.fightType = FightType.NORMAL; + if(fightIsAtLMS) { + this.fightType = defLvl <= FightType.LMS_1DEF.getCombatLevelsForType().def ? FightType.LMS_1DEF : + defLvl <= FightType.LMS_ZERK.getCombatLevelsForType().def ? FightType.LMS_ZERK : + FightType.LMS_MAXMED; + } + if(fightIsAtArena) { + defLvl = PLUGIN.getClient().getRealSkillLevel(Skill.DEFENCE); + this.fightType = defLvl <= FightType.ARENA_1DEF.getCombatLevelsForType().def ? FightType.ARENA_1DEF : + defLvl <= FightType.ARENA_ZERK.getCombatLevelsForType().def ? FightType.ARENA_ZERK : + FightType.ARENA_MAXMED; + } // initialize world this.world = PLUGIN.getClient().getWorld(); @@ -134,6 +155,12 @@ public FightPerformance(Player competitor, Player opponent) this.competitor = new Fighter(this, competitor); this.opponent = new Fighter(this, opponent); + if(!fightIsAtLMS && !fightIsAtArena) { + setOpponentsStats(new CombatLevels(opponent.getName(), hiscoreManager)); + } else { + setOpponentsStats(fightType.getCombatLevelsForType()); + } + this.competitorPrevHp = PLUGIN.getClient().getBoostedSkillLevel(Skill.HITPOINTS); this.competitor.setLastGhostBarrageCheckedMageXp(PLUGIN.getClient().getSkillExperience(Skill.MAGIC)); } @@ -150,9 +177,9 @@ public FightPerformance(FightPerformance__1_5_5 old) { int defLvl = competitor.getFightLogEntries().get(0).getAttackerLevels().def; this.fightType = - defLvl <= FightType.LMS_1DEF.getCombatLevelsForType().def ? FightType.LMS_1DEF : - defLvl <= FightType.LMS_ZERK.getCombatLevelsForType().def ? FightType.LMS_ZERK : - FightType.LMS_MAXMED; + defLvl <= FightType.LMS_1DEF.getCombatLevelsForType().def ? FightType.LMS_1DEF : + defLvl <= FightType.LMS_ZERK.getCombatLevelsForType().def ? FightType.LMS_ZERK : + FightType.LMS_MAXMED; } else { @@ -239,10 +266,10 @@ public void checkForAttackAnimations(Player eventSource, CombatLevels competitor { int offensivePray = PLUGIN.currentlyUsedOffensivePray(); competitor.addAttack( - opponent.getPlayer(), - animationData, - offensivePray, - competitorLevels); + opponent.getPlayer(), + animationData, + offensivePray, + competitorLevels, opponentsStats); lastFightTime = Instant.now().toEpochMilli(); addedAttack = true; @@ -255,7 +282,7 @@ else if (eName.equals(opponent.getName()) && Objects.equals(interactingName, com if (animationData != null) { // there is no offensive prayer data for the opponent so hardcode 0 - opponent.addAttack(competitor.getPlayer(), animationData, 0); + opponent.addAttack(competitor.getPlayer(), animationData, 0, competitorLevels, opponentsStats); addedAttack = true; // add a defensive log for the competitor while the opponent is attacking, to be used with the fight analysis/merge competitor.addDefensiveLogs(competitorLevels, PLUGIN.currentlyUsedOffensivePray()); @@ -296,10 +323,10 @@ public void checkForLocalGhostBarrage(CombatLevels competitorLevels, Player loca int offensivePray = PLUGIN.currentlyUsedOffensivePray(); competitor.addGhostBarrage(opponent.getPlayer().getOverheadIcon() != animationData.attackStyle.getProtection(), - opponent.getPlayer(), - AnimationData.MAGIC_ANCIENT_MULTI_TARGET, - offensivePray, - competitorLevels); + opponent.getPlayer(), + AnimationData.MAGIC_ANCIENT_MULTI_TARGET, + offensivePray, + competitorLevels); } } @@ -419,9 +446,9 @@ public boolean opponentDmgDealtIsGreater() public boolean competitorMagicHitsLuckier() { double competitorRate = (competitor.getMagicHitCountDeserved() == 0) ? 0 : - (competitor.getMagicHitCount() / competitor.getMagicHitCountDeserved()); + (competitor.getMagicHitCount() / competitor.getMagicHitCountDeserved()); double opponentRate = (opponent.getMagicHitCountDeserved() == 0) ? 0 : - (opponent.getMagicHitCount() / opponent.getMagicHitCountDeserved()); + (opponent.getMagicHitCount() / opponent.getMagicHitCountDeserved()); return competitorRate > opponentRate; } @@ -429,9 +456,9 @@ public boolean competitorMagicHitsLuckier() public boolean opponentMagicHitsLuckier() { double competitorRate = (competitor.getMagicHitCountDeserved() == 0) ? 0 : - (competitor.getMagicHitCount() / competitor.getMagicHitCountDeserved()); + (competitor.getMagicHitCount() / competitor.getMagicHitCountDeserved()); double opponentRate = (opponent.getMagicHitCountDeserved() == 0) ? 0 : - (opponent.getMagicHitCount() / opponent.getMagicHitCountDeserved()); + (opponent.getMagicHitCount() / opponent.getMagicHitCountDeserved()); return opponentRate > competitorRate; } @@ -455,7 +482,7 @@ public int compareTo(FightPerformance o) // if diff = 0, return 0. Otherwise, divide diff by its absolute value. This will result in // -1 for negative numbers, and 1 for positive numbers, keeping the sign and a safely small int. return diff == 0 ? 0 : - (int)(diff / Math.abs(diff)); + (int)(diff / Math.abs(diff)); } /** @@ -558,7 +585,7 @@ else if (defender == opponent) } } - public void updateKoChanceStats(FightLogEntry entry) + public void updateKoChanceStats(FightLogEntry entry) { if (entry.getDisplayKoChance() == null) { return; } diff --git a/src/main/java/matsyir/pvpperformancetracker/controllers/Fighter.java b/src/main/java/matsyir/pvpperformancetracker/controllers/Fighter.java index c3fbe38..2c0a1eb 100644 --- a/src/main/java/matsyir/pvpperformancetracker/controllers/Fighter.java +++ b/src/main/java/matsyir/pvpperformancetracker/controllers/Fighter.java @@ -74,7 +74,7 @@ class Fighter @Expose @SerializedName("s") private int offPraySuccessCount; // total number of successful off-pray attacks - // (when you use a different combat style than your opponent's overhead) + // (when you use a different combat style than your opponent's overhead) @Expose @SerializedName("d") private double deservedDamage; // total deserved damage based on gear & opponent's pray @@ -195,11 +195,11 @@ public Fighter(FightPerformance fight, String name) // Used for regular, ongoing fights void addAttack(Player opponent, AnimationData animationData, int offensivePray) { - addAttack(opponent, animationData, offensivePray, null); + addAttack(opponent, animationData, offensivePray, null, null); } // Levels can be null - void addAttack(Player opponent, AnimationData animationData, int offensivePray, CombatLevels levels) + void addAttack(Player opponent, AnimationData animationData, int offensivePray, CombatLevels levels, CombatLevels opponentLevels) { int[] attackerItems = player.getPlayerComposition().getEquipmentIds(); @@ -251,7 +251,7 @@ else if (weapon == EquipmentData.DRAGON_CROSSBOW && animationData = animationData.isSpecial ? AnimationData.MELEE_VLS_SPEC : AnimationData.MELEE_SCIM_SLASH; } - pvpDamageCalc.updateDamageStats(player, opponent, successful, animationData); + pvpDamageCalc.updateDamageStats(player, opponent, successful, animationData, levels, opponentLevels); deservedDamage += pvpDamageCalc.getAverageHit(); if (animationData.attackStyle == AnimationData.AttackStyle.MAGIC) @@ -265,7 +265,7 @@ else if (weapon == EquipmentData.DRAGON_CROSSBOW && } } - FightLogEntry fightLogEntry = new FightLogEntry(player, opponent, pvpDamageCalc, offensivePray, levels, animationData); + FightLogEntry fightLogEntry = new FightLogEntry(player, opponent, pvpDamageCalc, offensivePray, levels, opponentLevels, animationData); fightLogEntry.setGmaulSpecial(isGmaulSpec); if (PvpPerformanceTrackerPlugin.CONFIG.fightLogInChat()) { @@ -311,7 +311,7 @@ public void addGhostBarrage(boolean successful, Player opponent, AnimationData a } lastGhostBarrageCheckedTick = currentTick; - pvpDamageCalc.updateDamageStats(player, opponent, successful, animationData); + pvpDamageCalc.updateDamageStats(player, opponent, successful, animationData, levels, levels); ghostBarrageCount++; ghostBarrageDeservedDamage += pvpDamageCalc.getAverageHit(); @@ -382,8 +382,8 @@ public String getOffPrayStats(boolean shortString) { nf.setMaximumFractionDigits(1); return shortString ? - offPraySuccessCount + "/" + attackCount : - offPraySuccessCount + "/" + attackCount + " (" + nf.format(calculateOffPraySuccessPercentage()) + "%)"; + offPraySuccessCount + "/" + attackCount : + offPraySuccessCount + "/" + attackCount + " (" + nf.format(calculateOffPraySuccessPercentage()) + "%)"; } public String getOffPrayStats() @@ -399,8 +399,8 @@ public String getMagicHitStats() stats += "/" + nf.format(magicAttackCount); nf.setMaximumFractionDigits(1); String luckPercentage = magicHitCountDeserved != 0 ? - nf.format(((double)magicHitCount / magicHitCountDeserved) * 100.0) : - "0"; + nf.format(((double)magicHitCount / magicHitCountDeserved) * 100.0) : + "0"; stats += " (" + luckPercentage + "%)"; return stats; } @@ -409,8 +409,8 @@ public String getShortMagicHitStats() { nf.setMaximumFractionDigits(1); return magicHitCountDeserved != 0 ? - nf.format(((double)magicHitCount / magicHitCountDeserved) * 100.0) + "%" : - "0%"; + nf.format(((double)magicHitCount / magicHitCountDeserved) * 100.0) + "%" : + "0%"; } public String getDeservedDmgString(Fighter opponent, int precision, boolean onlyDiff) @@ -418,7 +418,7 @@ public String getDeservedDmgString(Fighter opponent, int precision, boolean only nf.setMaximumFractionDigits(precision); double difference = deservedDamage - opponent.deservedDamage; return onlyDiff ? (difference > 0 ? "+" : "") + nf.format(difference) : - nf.format(deservedDamage) + " (" + (difference > 0 ? "+" : "") + nf.format(difference) + ")"; + nf.format(deservedDamage) + " (" + (difference > 0 ? "+" : "") + nf.format(difference) + ")"; } public String getDeservedDmgString(Fighter opponent) { @@ -430,7 +430,7 @@ public String getDmgDealtString(Fighter opponent, boolean onlyDiff) { int difference = damageDealt - opponent.damageDealt; return onlyDiff ? (difference > 0 ? "+" : "") + difference: - damageDealt + " (" + (difference > 0 ? "+" : "") + difference + ")"; + damageDealt + " (" + (difference > 0 ? "+" : "") + difference + ")"; } public String getDmgDealtString(Fighter opponent) { @@ -440,13 +440,13 @@ public String getDmgDealtString(Fighter opponent) public double calculateOffPraySuccessPercentage() { return attackCount == 0 ? 0 : - (double) offPraySuccessCount / attackCount * 100.0; + (double) offPraySuccessCount / attackCount * 100.0; } public double calculateOffensivePraySuccessPercentage() { return attackCount == 0 ? 0 : - (double) offensivePraySuccessCount / attackCount * 100.0; + (double) offensivePraySuccessCount / attackCount * 100.0; } public int getMagicAttackCount() @@ -461,8 +461,8 @@ public String getOffensivePrayStats(boolean shortString) { nf.setMaximumFractionDigits(1); return shortString ? - offensivePraySuccessCount + "/" + attackCount : - offensivePraySuccessCount + "/" + attackCount + " (" + nf.format(calculateOffensivePraySuccessPercentage()) + "%)"; + offensivePraySuccessCount + "/" + attackCount : + offensivePraySuccessCount + "/" + attackCount + " (" + nf.format(calculateOffensivePraySuccessPercentage()) + "%)"; } public String getOffensivePrayStats() diff --git a/src/main/java/matsyir/pvpperformancetracker/controllers/PvpDamageCalc.java b/src/main/java/matsyir/pvpperformancetracker/controllers/PvpDamageCalc.java index 11482bc..fb44d33 100644 --- a/src/main/java/matsyir/pvpperformancetracker/controllers/PvpDamageCalc.java +++ b/src/main/java/matsyir/pvpperformancetracker/controllers/PvpDamageCalc.java @@ -41,13 +41,11 @@ import matsyir.pvpperformancetracker.models.CombatLevels; import matsyir.pvpperformancetracker.models.RangeAmmoData; import matsyir.pvpperformancetracker.models.RingData; -import net.runelite.api.PlayerComposition; -import net.runelite.api.SpriteID; +import net.runelite.api.*; import net.runelite.api.kit.KitType; import net.runelite.client.game.ItemEquipmentStats; import net.runelite.client.game.ItemStats; import org.apache.commons.lang3.ArrayUtils; -import net.runelite.api.Player; // Pvp damage calculations // call updateDamageStats(...) with required parameters, and retrieve results by using the field getters @@ -59,7 +57,7 @@ public class PvpDamageCalc { private static final int STAB_ATTACK = 0, SLASH_ATTACK = 1, CRUSH_ATTACK = 2, MAGIC_ATTACK = 3, - RANGE_ATTACK = 4, STAB_DEF = 5, SLASH_DEF = 6, CRUSH_DEF = 7, MAGIC_DEF = 8; + RANGE_ATTACK = 4, STAB_DEF = 5, SLASH_DEF = 6, CRUSH_DEF = 7, MAGIC_DEF = 8; public static final int RANGE_DEF = 9; private static final int STRENGTH_BONUS = 10, RANGE_STRENGTH = 11, MAGIC_DAMAGE = 12; @@ -139,22 +137,39 @@ public class PvpDamageCalc private RingData ringUsed; boolean isLmsFight; + boolean isArenaFight; public PvpDamageCalc(FightPerformance relatedFight) { isLmsFight = relatedFight.fightType.isLmsFight(); - this.attackerLevels = relatedFight.fightType.getCombatLevelsForType(); - this.defenderLevels = relatedFight.fightType.getCombatLevelsForType(); + isArenaFight = relatedFight.fightType.isArenaFight(); + if(isLmsFight || isArenaFight) { + this.attackerLevels = relatedFight.fightType.getCombatLevelsForType(); + this.defenderLevels = relatedFight.fightType.getCombatLevelsForType(); + } else { + this.attackerLevels = relatedFight.getPlayersStats(); + this.defenderLevels = relatedFight.getOpponentsStats(); + } this.ringUsed = isLmsFight ? RingData.BERSERKER_RING : CONFIG.ringChoice(); } // main function used to update stats during an ongoing fight - public void updateDamageStats(Player attacker, Player defender, boolean success, AnimationData animationData) + public void updateDamageStats(Player attacker, Player defender, boolean success, AnimationData animationData, CombatLevels competitorLevels, CombatLevels opponentLevels) { // shouldn't be possible, but just in case if (attacker == null || defender == null) { return; } + //Use appropriate stats based on who is the attacker or defender + String competitor = PLUGIN.getClient().getLocalPlayer().getName(); + if(attacker.getName().equals(competitor)) { + this.attackerLevels = competitorLevels; + this.defenderLevels = opponentLevels; + } else { + this.attackerLevels = opponentLevels; + this.defenderLevels = competitorLevels; + } + averageHit = 0; accuracy = 0; minHit = 0; @@ -200,15 +215,16 @@ else if (attackStyle == AttackStyle.MAGIC) minHit = (int)(minHit * (success ? 1 : UNSUCCESSFUL_PRAY_DMG_MODIFIER)); log.debug("attackStyle: " + attackStyle.toString() + ", avgHit: " + nf.format(averageHit) + ", acc: " + nf.format(accuracy) + - "\nattacker(" + attacker.getName() + ")stats: " + Arrays.toString(playerStats) + - "\ndefender(" + defender.getName() + ")stats: " + Arrays.toString(opponentStats)); + "\nattacker(" + attacker.getName() + ")stats: " + Arrays.toString(playerStats) + + "\ndefender(" + defender.getName() + ")stats: " + Arrays.toString(opponentStats)); } // secondary function used to analyze fights from the fight log (fight analysis/fight merge) public void updateDamageStats(FightLogEntry atkLog, FightLogEntry defenderLog) { this.attackerLevels = atkLog.getAttackerLevels(); - this.defenderLevels = defenderLog.getAttackerLevels(); + this.defenderLevels = defenderLog.getDefenderLevels(); + int[] attackerItems = atkLog.getAttackerGear(); int[] defenderItems = atkLog.getDefenderGear(); boolean success = atkLog.success(); @@ -222,8 +238,8 @@ public void updateDamageStats(FightLogEntry atkLog, FightLogEntry defenderLog) EquipmentData weapon = EquipmentData.fromId(fixItemId(attackerItems[KitType.WEAPON.getIndex()])); - int[] playerStats = this.calculateBonuses(attackerItems); - int[] opponentStats = this.calculateBonuses(defenderItems); + int[] playerEquipStats = this.calculateBonuses(attackerItems); + int[] opponentEquipStats = this.calculateBonuses(defenderItems); AnimationData.AttackStyle attackStyle = animationData.attackStyle; // basic style: stab/slash/crush/ranged/magic // Special attack used will be determined based on the currently used weapon, if its special attack has been implemented. @@ -234,21 +250,21 @@ public void updateDamageStats(FightLogEntry atkLog, FightLogEntry defenderLog) if (attackStyle.isMelee()) { - getMeleeMaxHit(playerStats[STRENGTH_BONUS], isSpecial, weapon, voidStyle, successfulOffensive); - getMeleeAccuracy(playerStats, opponentStats, attackStyle, isSpecial, weapon, voidStyle, successfulOffensive); + getMeleeMaxHit(playerEquipStats[STRENGTH_BONUS], isSpecial, weapon, voidStyle, successfulOffensive); + getMeleeAccuracy(playerEquipStats, opponentEquipStats, attackStyle, isSpecial, weapon, voidStyle, successfulOffensive); } else if (attackStyle == AttackStyle.RANGED) { - getRangedMaxHit(playerStats[RANGE_STRENGTH], isSpecial, weapon, voidStyle, successfulOffensive, attackerItems); - getRangeAccuracy(playerStats[RANGE_ATTACK], opponentStats[RANGE_DEF], isSpecial, weapon, voidStyle, successfulOffensive, attackerItems); + getRangedMaxHit(playerEquipStats[RANGE_STRENGTH], isSpecial, weapon, voidStyle, successfulOffensive, attackerItems); + getRangeAccuracy(playerEquipStats[RANGE_ATTACK], opponentEquipStats[RANGE_DEF], isSpecial, weapon, voidStyle, successfulOffensive, attackerItems); } // this should always be true at this point, but just in case. unknown animation styles won't // make it here, they should be stopped in FightPerformance::checkForAttackAnimations else if (attackStyle == AttackStyle.MAGIC) { EquipmentData shield = EquipmentData.fromId(fixItemId(attackerItems[KitType.SHIELD.getIndex()])); - getMagicMaxHit(shield, playerStats[MAGIC_DAMAGE], animationData, weapon, voidStyle, successfulOffensive); - getMagicAccuracy(playerStats[MAGIC_ATTACK], opponentStats[MAGIC_DEF], weapon, animationData, voidStyle, successfulOffensive, defenderLog.getAttackerOffensivePray() == SpriteID.PRAYER_AUGURY); + getMagicMaxHit(shield, playerEquipStats[MAGIC_DAMAGE], animationData, weapon, voidStyle, successfulOffensive); + getMagicAccuracy(playerEquipStats[MAGIC_ATTACK], opponentEquipStats[MAGIC_DEF], weapon, animationData, voidStyle, successfulOffensive, defenderLog.getAttackerOffensivePray() == SpriteID.PRAYER_AUGURY); } getAverageHit(success, weapon, isSpecial); @@ -338,10 +354,10 @@ else if (burningClaws && usingSpec) double avgTotalDmg4 = getAverageBurningClawDamage(minD4, maxD4); double expectedDamage = - (acc) * avgTotalDmg1 + - (miss * acc) * avgTotalDmg2 + - (miss * miss * acc) * avgTotalDmg3 + - (miss * miss * miss) * avgTotalDmg4; + (acc) * avgTotalDmg1 + + (miss * acc) * avgTotalDmg2 + + (miss * miss * acc) * avgTotalDmg3 + + (miss * miss * miss) * avgTotalDmg4; this.averageHit = expectedDamage * prayerModifier; @@ -391,7 +407,7 @@ else if (usingSpec && voidwaker) if (minHit > 0) { log.info("PvpDamageCalc:getAverageHit: Fell into default avg hit calculation with a minHit > 0 (" + - minHit + "). Shouldn't happen. Weapon: " + weapon.toString()); + minHit + "). Shouldn't happen. Weapon: " + weapon.toString()); } } @@ -466,7 +482,7 @@ private void getRangedMaxHit(int rangeStrength, boolean usingSpec, EquipmentData if (this.isLmsFight) { weaponAmmo = weaponAmmo instanceof RangeAmmoData.StrongBoltAmmo ? RangeAmmoData.StrongBoltAmmo.OPAL_DRAGON_BOLTS_E : - weaponAmmo instanceof RangeAmmoData.BoltAmmo ? RangeAmmoData.BoltAmmo.DIAMOND_BOLTS_E : weaponAmmo; + weaponAmmo instanceof RangeAmmoData.BoltAmmo ? RangeAmmoData.BoltAmmo.DIAMOND_BOLTS_E : weaponAmmo; } boolean ballista = weapon == EquipmentData.HEAVY_BALLISTA; @@ -477,6 +493,8 @@ private void getRangedMaxHit(int rangeStrength, boolean usingSpec, EquipmentData rangeStrength += ammoStrength; + System.out.println("getRangedMaxHit Range level: " + attackerLevels.range); + double effectiveLevel = Math.floor((attackerLevels.range * (successfulOffensive ? RIGOUR_OFFENSIVE_PRAYER_DMG_MODIFIER : 1)) + 8); // apply void bonus if applicable if (voidStyle == VoidStyle.VOID_ELITE_RANGE || voidStyle == VoidStyle.VOID_RANGE) @@ -510,19 +528,19 @@ private void getRangedMaxHit(int rangeStrength, boolean usingSpec, EquipmentData } else // Standard Ranged Max Hit Calc { - maxHit = weaponAmmo == null ? - (int) (modifier * baseDamage) : - (int) ((modifier * baseDamage) + weaponAmmo.getBonusMaxHit(attackerLevels.range)); + maxHit = weaponAmmo == null ? + (int) (modifier * baseDamage) : + (int) ((modifier * baseDamage) + weaponAmmo.getBonusMaxHit(attackerLevels.range)); } // apply crystal armor bonus if using bow if ((weapon == EquipmentData.BOW_OF_FAERDHINEN || weapon == EquipmentData.CRYSTAL_BOW || weapon == EquipmentData.CRYSTAL_BOW_I) && - (head == EquipmentData.CRYSTAL_HELM || body == EquipmentData.CRYSTAL_BODY || legs == EquipmentData.CRYSTAL_LEGS)) + (head == EquipmentData.CRYSTAL_HELM || body == EquipmentData.CRYSTAL_BODY || legs == EquipmentData.CRYSTAL_LEGS)) { double dmgModifier = 1 + - (head == EquipmentData.CRYSTAL_HELM ? 0.025 : 0) + - (body == EquipmentData.CRYSTAL_BODY ? 0.075 : 0) + - (legs == EquipmentData.CRYSTAL_LEGS ? 0.05 : 0); + (head == EquipmentData.CRYSTAL_HELM ? 0.025 : 0) + + (body == EquipmentData.CRYSTAL_BODY ? 0.075 : 0) + + (legs == EquipmentData.CRYSTAL_LEGS ? 0.05 : 0); maxHit *= dmgModifier; } @@ -596,6 +614,9 @@ private void getMeleeAccuracy(int[] playerStats, int[] opponentStats, AttackStyl /** * Attacker Chance */ + + System.out.println("getMeleeAccuracy Attack Level: " + attackerLevels.atk); + effectiveLevelPlayer = Math.floor(((attackerLevels.atk * (successfulOffensive ? PIETY_ATK_PRAYER_MODIFIER : 1)) + STANCE_BONUS) + 8); // apply void bonus if applicable if (voidStyle == VoidStyle.VOID_ELITE_MELEE || voidStyle == VoidStyle.VOID_MELEE) @@ -604,10 +625,10 @@ private void getMeleeAccuracy(int[] playerStats, int[] opponentStats, AttackStyl } final double attackBonus = attackStyle == AttackStyle.STAB ? stabBonusPlayer - : attackStyle == AttackStyle.SLASH ? slashBonusPlayer : crushBonusPlayer; + : attackStyle == AttackStyle.SLASH ? slashBonusPlayer : crushBonusPlayer; final double targetDefenceBonus = attackStyle == AttackStyle.STAB ? stabBonusTarget - : attackStyle == AttackStyle.SLASH ? slashBonusTarget : crushBonusTarget; + : attackStyle == AttackStyle.SLASH ? slashBonusTarget : crushBonusTarget; baseChance = Math.floor(effectiveLevelPlayer * (attackBonus + 64)); @@ -615,7 +636,7 @@ private void getMeleeAccuracy(int[] playerStats, int[] opponentStats, AttackStyl { // Don't apply the generic modifier if it's the Abyssal Dagger (handled separately below) if (weapon != EquipmentData.ABYSSAL_DAGGER) { - baseChance = baseChance * accuracyModifier; + baseChance = baseChance * accuracyModifier; } } @@ -629,6 +650,8 @@ private void getMeleeAccuracy(int[] playerStats, int[] opponentStats, AttackStyl /** * Defender Chance */ + + System.out.println("getMeleeAccuracy Defender defense Level: " + defenderLevels.def); effectiveLevelTarget = Math.floor(((defenderLevels.def * PIETY_DEF_PRAYER_MODIFIER) + STANCE_BONUS) + 8); if (vls && usingSpec) @@ -658,7 +681,7 @@ private void getRangeAccuracy(int playerRangeAtt, int opponentRangeDef, boolean RangeAmmoData weaponAmmo = EquipmentData.getWeaponAmmo(weapon); // if it's an LMS fight and bolts are used, don't use config bolt, just use diamond bolts(e) if (this.isLmsFight && (weaponAmmo instanceof RangeAmmoData.BoltAmmo || - weaponAmmo instanceof RangeAmmoData.StrongBoltAmmo)) + weaponAmmo instanceof RangeAmmoData.StrongBoltAmmo)) { weaponAmmo = RangeAmmoData.BoltAmmo.DIAMOND_BOLTS_E; } @@ -674,6 +697,7 @@ private void getRangeAccuracy(int playerRangeAtt, int opponentRangeDef, boolean /** * Attacker Chance */ + System.out.println("getRangeAccuracy range Level: " + attackerLevels.range); effectiveLevelPlayer = Math.floor(((attackerLevels.range * (successfulOffensive ? RIGOUR_OFFENSIVE_PRAYER_ATTACK_MODIFIER : 1)) + STANCE_BONUS) + 8); // apply void bonus if applicable if (voidStyle == VoidStyle.VOID_ELITE_RANGE || voidStyle == VoidStyle.VOID_RANGE) @@ -687,12 +711,12 @@ private void getRangeAccuracy(int playerRangeAtt, int opponentRangeDef, boolean EquipmentData legs = EquipmentData.fromId(fixItemId(attackerComposition[KitType.LEGS.getIndex()])); if ((weapon == EquipmentData.BOW_OF_FAERDHINEN || weapon == EquipmentData.CRYSTAL_BOW || weapon == EquipmentData.CRYSTAL_BOW_I) && - (head == EquipmentData.CRYSTAL_HELM || body == EquipmentData.CRYSTAL_BODY || legs == EquipmentData.CRYSTAL_LEGS)) + (head == EquipmentData.CRYSTAL_HELM || body == EquipmentData.CRYSTAL_BODY || legs == EquipmentData.CRYSTAL_LEGS)) { double accuracyModifier = 1 + - (head == EquipmentData.CRYSTAL_HELM ? 0.05 : 0) + - (body == EquipmentData.CRYSTAL_BODY ? 0.15 : 0) + - (legs == EquipmentData.CRYSTAL_LEGS ? 0.1 : 0); + (head == EquipmentData.CRYSTAL_HELM ? 0.05 : 0) + + (body == EquipmentData.CRYSTAL_BODY ? 0.15 : 0) + + (legs == EquipmentData.CRYSTAL_LEGS ? 0.1 : 0); effectiveLevelPlayer *= accuracyModifier; } @@ -704,7 +728,7 @@ private void getRangeAccuracy(int playerRangeAtt, int opponentRangeDef, boolean boolean ballista = weapon == EquipmentData.HEAVY_BALLISTA; double specAccuracyModifier = acb ? ACB_SPEC_ACCURACY_MODIFIER : - ballista ? BALLISTA_SPEC_ACCURACY_MODIFIER : 1; + ballista ? BALLISTA_SPEC_ACCURACY_MODIFIER : 1; attackerChance = Math.floor(rangeModifier * specAccuracyModifier); } @@ -716,6 +740,7 @@ private void getRangeAccuracy(int playerRangeAtt, int opponentRangeDef, boolean /** * Defender Chance */ + System.out.println("getRangeAccuracy defender Level: " + defenderLevels.def); effectiveLevelTarget = Math.floor(((defenderLevels.def * RIGOUR_DEF_PRAYER_MODIFIER) + STANCE_BONUS) + 8); defenderChance = Math.floor(effectiveLevelTarget * ((double) opponentRangeDef + 64)); @@ -759,6 +784,7 @@ private void getMagicAccuracy(int playerMageAtt, int opponentMageDef, EquipmentD /** * Attacker Chance */ + System.out.println("getMagicAccuracy magic Level: " + attackerLevels.mage); effectiveLevelPlayer = Math.floor(((attackerLevels.mage * (successfulOffensive ? AUGURY_OFFENSIVE_PRAYER_MODIFIER : 1))) + 8); // apply void bonus if applicable if (voidStyle == VoidStyle.VOID_ELITE_MAGE || voidStyle == VoidStyle.VOID_MAGE) @@ -772,6 +798,8 @@ private void getMagicAccuracy(int playerMageAtt, int opponentMageDef, EquipmentD /** * Defender Chance */ + System.out.println("getMagicAccuracy defender defense Level: " + defenderLevels.def); + System.out.println("getMagicAccuracy defender magic Level: " + defenderLevels.mage); effectiveLevelTarget = Math.floor(((defenderLevels.def * AUGURY_DEF_PRAYER_MODIFIER) + STANCE_BONUS) + 8); effectiveMagicLevelTarget = Math.floor((defenderLevels.mage * (defensiveAugurySuccess ? AUGURY_MAGEDEF_PRAYER_MODIFIER : 1)) * 0.70); reducedDefenceLevelTarget = Math.floor(effectiveLevelTarget * 0.30); @@ -779,8 +807,8 @@ private void getMagicAccuracy(int playerMageAtt, int opponentMageDef, EquipmentD // 0.975x is a simplified brimstone accuracy formula, where x = mage def defenderChance = ringUsed == RingData.BRIMSTONE_RING ? - Math.floor(effectiveMagicDefenceTarget * ((BRIMSTONE_RING_OPPONENT_DEF_MODIFIER * opponentMageDef) + 64)) : - Math.floor(effectiveMagicDefenceTarget * ((double) opponentMageDef + 64)); + Math.floor(effectiveMagicDefenceTarget * ((BRIMSTONE_RING_OPPONENT_DEF_MODIFIER * opponentMageDef) + 64)) : + Math.floor(effectiveMagicDefenceTarget * ((double) opponentMageDef + 64)); /** * Calculate Accuracy @@ -834,19 +862,19 @@ public static int[] getItemStats(int itemId) return null; } return new int[] { - equipmentStats.getAstab(), // 0 - equipmentStats.getAslash(), // 1 - equipmentStats.getAcrush(), // 2 - equipmentStats.getAmagic(), // 3 - equipmentStats.getArange(), // 4 - equipmentStats.getDstab(), // 5 - equipmentStats.getDslash(), // 6 - equipmentStats.getDcrush(), // 7 - equipmentStats.getDmagic(), // 8 - equipmentStats.getDrange(), // 9 - equipmentStats.getStr(), // 10 - equipmentStats.getRstr(), // 11 - (int)equipmentStats.getMdmg(), // 12 + equipmentStats.getAstab(), // 0 + equipmentStats.getAslash(), // 1 + equipmentStats.getAcrush(), // 2 + equipmentStats.getAmagic(), // 3 + equipmentStats.getArange(), // 4 + equipmentStats.getDstab(), // 5 + equipmentStats.getDslash(), // 6 + equipmentStats.getDcrush(), // 7 + equipmentStats.getDmagic(), // 8 + equipmentStats.getDrange(), // 9 + equipmentStats.getStr(), // 10 + equipmentStats.getRstr(), // 11 + (int)equipmentStats.getMdmg(), // 12 }; } @@ -865,8 +893,8 @@ public static int[] calculateBonuses(int[] itemIds) public static int[] calculateBonuses(int[] itemIds, RingData ringUsed) { int[] equipmentBonuses = ringUsed == null || ringUsed == RingData.NONE ? - new int[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } : - getItemStats(ringUsed.getItemId()); + new int[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } : + getItemStats(ringUsed.getItemId()); if (equipmentBonuses == null) // shouldn't happen, but as a failsafe if the ring lookup fails { @@ -898,19 +926,19 @@ public static ItemEquipmentStats calculateBonusesToStats(int[] itemIds) { int[] bonuses = calculateBonuses(itemIds); return ItemEquipmentStats.builder() - .astab(bonuses[STAB_ATTACK]) // 0 - .aslash(bonuses[SLASH_ATTACK]) // 1 - .acrush(bonuses[CRUSH_ATTACK]) // 2 - .amagic(bonuses[MAGIC_ATTACK]) // 3 - .arange(bonuses[RANGE_ATTACK]) // 4 - .dstab(bonuses[STAB_DEF]) // 5 - .dslash(bonuses[SLASH_DEF]) // 6 - .dcrush(bonuses[CRUSH_DEF]) // 7 - .dmagic(bonuses[MAGIC_DEF]) // 8 - .drange(bonuses[RANGE_DEF]) // 9 - .str(bonuses[STRENGTH_BONUS]) // 10 - .rstr(bonuses[RANGE_STRENGTH]) // 11 - .mdmg(bonuses[MAGIC_DAMAGE]) // 12 - .build(); + .astab(bonuses[STAB_ATTACK]) // 0 + .aslash(bonuses[SLASH_ATTACK]) // 1 + .acrush(bonuses[CRUSH_ATTACK]) // 2 + .amagic(bonuses[MAGIC_ATTACK]) // 3 + .arange(bonuses[RANGE_ATTACK]) // 4 + .dstab(bonuses[STAB_DEF]) // 5 + .dslash(bonuses[SLASH_DEF]) // 6 + .dcrush(bonuses[CRUSH_DEF]) // 7 + .dmagic(bonuses[MAGIC_DEF]) // 8 + .drange(bonuses[RANGE_DEF]) // 9 + .str(bonuses[STRENGTH_BONUS]) // 10 + .rstr(bonuses[RANGE_STRENGTH]) // 11 + .mdmg(bonuses[MAGIC_DAMAGE]) // 12 + .build(); } } diff --git a/src/main/java/matsyir/pvpperformancetracker/models/CombatLevels.java b/src/main/java/matsyir/pvpperformancetracker/models/CombatLevels.java index 071cd16..0bc61b4 100644 --- a/src/main/java/matsyir/pvpperformancetracker/models/CombatLevels.java +++ b/src/main/java/matsyir/pvpperformancetracker/models/CombatLevels.java @@ -28,8 +28,13 @@ import com.google.gson.annotations.SerializedName; import lombok.Getter; import static matsyir.pvpperformancetracker.PvpPerformanceTrackerPlugin.CONFIG; + import net.runelite.api.Client; import net.runelite.api.Skill; +import net.runelite.client.hiscore.HiscoreResult; +import net.runelite.client.hiscore.HiscoreSkill; +import net.runelite.client.hiscore.HiscoreEndpoint; +import net.runelite.client.hiscore.HiscoreManager; // Basic class that will be used to save current combat levels (including boosts/drains) @Getter @@ -38,11 +43,11 @@ public class CombatLevels public static CombatLevels getConfigLevels() { return new CombatLevels(CONFIG.attackLevel(), - CONFIG.strengthLevel(), - CONFIG.defenceLevel(), - CONFIG.rangedLevel(), - CONFIG.magicLevel(), - 99); + CONFIG.strengthLevel(), + CONFIG.defenceLevel(), + CONFIG.rangedLevel(), + CONFIG.magicLevel(), + 99); } @Expose @@ -84,6 +89,44 @@ public CombatLevels(Client client) this.hp = client.getBoostedSkillLevel(Skill.HITPOINTS); } + public CombatLevels(String username, HiscoreManager hiscoreManager) { + try { + HiscoreResult lookupResults = hiscoreManager.lookupAsync(username.toLowerCase(), HiscoreEndpoint.NORMAL); + if(lookupResults == null) { + //if highscores fails just pull from config + this.atk = CONFIG.attackLevel(); + this.str = CONFIG.strengthLevel(); + this.def = CONFIG.defenceLevel(); + this.range = CONFIG.rangedLevel(); + this.mage = CONFIG.magicLevel(); + this.hp = 99; + } else { + int usernamesAttack = lookupResults.getSkill(HiscoreSkill.ATTACK).getLevel(); + int usernamesStrength = lookupResults.getSkill(HiscoreSkill.STRENGTH).getLevel(); + int usernamesDefence = lookupResults.getSkill(HiscoreSkill.DEFENCE).getLevel(); + int usernamesRange = lookupResults.getSkill(HiscoreSkill.RANGED).getLevel(); + int usernamesMagic = lookupResults.getSkill(HiscoreSkill.MAGIC).getLevel(); + int usernamesHitpoints = lookupResults.getSkill(HiscoreSkill.HITPOINTS).getLevel(); + + //assume boosted + this.atk = (int) Math.floor((usernamesAttack + 5) * 1.15); + this.str = (int) Math.floor((usernamesStrength + 5) * 1.15); + this.def = (int) Math.floor((usernamesDefence + 2) * 1.20); + this.range = (int) Math.floor((usernamesRange + 4) * 1.10); + this.mage = usernamesMagic; //nobody uses boosted magic for extended periods of time + this.hp = usernamesHitpoints; + } + } catch (Exception exception) { + //if highscores fails just pull from config + this.atk = CONFIG.attackLevel(); + this.str = CONFIG.strengthLevel(); + this.def = CONFIG.defenceLevel(); + this.range = CONFIG.rangedLevel(); + this.mage = CONFIG.magicLevel(); + this.hp = 99; + } + } + public int getSkill(Skill skill) { switch(skill) @@ -97,4 +140,8 @@ public int getSkill(Skill skill) default: return 0; } } + + public String toString() { + return "(" + atk + ", " + str + ", " + def + ", " + range + ", " + mage + ", " + hp + ")"; + } } diff --git a/src/main/java/matsyir/pvpperformancetracker/models/FightLogEntry.java b/src/main/java/matsyir/pvpperformancetracker/models/FightLogEntry.java index e1c9acc..6e79511 100644 --- a/src/main/java/matsyir/pvpperformancetracker/models/FightLogEntry.java +++ b/src/main/java/matsyir/pvpperformancetracker/models/FightLogEntry.java @@ -112,6 +112,10 @@ public class FightLogEntry implements Comparable @SerializedName("C") private CombatLevels attackerLevels; // CAN BE NULL + @Expose + @SerializedName("D") + private CombatLevels defenderLevels; // CAN BE NULL + @Getter @Setter @Expose @@ -192,7 +196,7 @@ public class FightLogEntry implements Comparable @Getter @Setter private boolean isPartOfTickGroup = false; - public FightLogEntry(Player attacker, Player defender, PvpDamageCalc pvpDamageCalc, int attackerOffensivePray, CombatLevels levels, AnimationData animationData) + public FightLogEntry(Player attacker, Player defender, PvpDamageCalc pvpDamageCalc, int attackerOffensivePray, CombatLevels levels, CombatLevels defLevels, AnimationData animationData) { this.isFullEntry = true; @@ -221,6 +225,7 @@ public FightLogEntry(Player attacker, Player defender, PvpDamageCalc pvpDamageCa this.expectedHits = PvpPerformanceTrackerUtils.getExpectedHits(animationData); this.matchedHitsCount = 0; this.actualDamageSum = 0; + this.defenderLevels = defLevels; } // create incomplete entry to save competitor's defensive stats which are only client side @@ -296,20 +301,20 @@ public String toChatMessage() { Color darkRed = new Color(127, 0, 0); // same color as default clan chat color return new ChatMessageBuilder() - .append(darkRed, attackerName + ": ") - .append(Color.BLACK, "Style: ") - .append(darkRed, WordUtils.capitalizeFully(animationData.attackStyle.toString())) - .append(Color.BLACK, " Hit: ") - .append(darkRed, getHitRange()) - .append(Color.BLACK, " Acc: ") - .append(darkRed, nf.format(accuracy)) - .append(Color.BLACK, " AvgHit: ") - .append(darkRed, nf.format(deservedDamage)) - .append(Color.BLACK, " Spec?: ") - .append(darkRed, animationData.isSpecial ? "Y" : "N") - .append(Color.BLACK, " OffP?:") - .append(darkRed, success() ? "Y" : "N") - .build(); + .append(darkRed, attackerName + ": ") + .append(Color.BLACK, "Style: ") + .append(darkRed, WordUtils.capitalizeFully(animationData.attackStyle.toString())) + .append(Color.BLACK, " Hit: ") + .append(darkRed, getHitRange()) + .append(Color.BLACK, " Acc: ") + .append(darkRed, nf.format(accuracy)) + .append(Color.BLACK, " AvgHit: ") + .append(darkRed, nf.format(deservedDamage)) + .append(Color.BLACK, " Spec?: ") + .append(darkRed, animationData.isSpecial ? "Y" : "N") + .append(Color.BLACK, " OffP?:") + .append(darkRed, success() ? "Y" : "N") + .build(); } public String getHitRange() @@ -326,7 +331,7 @@ public int compareTo(FightLogEntry o) // if diff = 0, return 0. Otherwise, divide diff by its absolute value. This will result in // -1 for negative numbers, and 1 for positive numbers, keeping the sign and a safely small int. return diff == 0 ? 0 : - (int)(diff / Math.abs(diff)); + (int)(diff / Math.abs(diff)); } } diff --git a/src/main/java/matsyir/pvpperformancetracker/models/FightType.java b/src/main/java/matsyir/pvpperformancetracker/models/FightType.java index e5a3e1d..ed3311c 100644 --- a/src/main/java/matsyir/pvpperformancetracker/models/FightType.java +++ b/src/main/java/matsyir/pvpperformancetracker/models/FightType.java @@ -29,12 +29,16 @@ public enum FightType { + ARENA_MAXMED(new CombatLevels(91, 118, 86, 112, 99, 99)), + ARENA_ZERK(new CombatLevels(74, 118, 56, 112, 99, 99)), + ARENA_1DEF(new CombatLevels(74, 118, 6, 112, 99, 99)), LMS_MAXMED(new CombatLevels(118, 118, 75, 112, 99, 99)), - LMS_ZERK(new CombatLevels(91, 118, 45, 112, 99, 99)), + LMS_ZERK(new CombatLevels(91, 118, 50, 112, 99, 99)), LMS_1DEF(new CombatLevels(91, 118, 1, 112, 99, 99)), NORMAL(CombatLevels.getConfigLevels()); private static FightType[] LMS_TYPES = { LMS_MAXMED, LMS_ZERK, LMS_1DEF }; + private static FightType[] ARENA_TYPES = { ARENA_MAXMED, ARENA_ZERK, ARENA_1DEF }; private CombatLevels combatLevelsForType; FightType(CombatLevels combatLevelsForType) @@ -56,4 +60,8 @@ public boolean isLmsFight() { return ArrayUtils.contains(LMS_TYPES, this); } + public boolean isArenaFight() + { + return ArrayUtils.contains(ARENA_TYPES, this); + } } diff --git a/src/main/java/matsyir/pvpperformancetracker/views/FightLogDetailFrame.java b/src/main/java/matsyir/pvpperformancetracker/views/FightLogDetailFrame.java index 8b432fd..f5dc408 100644 --- a/src/main/java/matsyir/pvpperformancetracker/views/FightLogDetailFrame.java +++ b/src/main/java/matsyir/pvpperformancetracker/views/FightLogDetailFrame.java @@ -87,7 +87,7 @@ class FightLogDetailFrame extends JFrame FightLogDetailFrame(FightPerformance fight, FightLogEntry log, int rowIdx, Point location) { super("Fight Log Details - " + fight.getCompetitor().getName() + " vs " + fight.getOpponent().getName() - + " on world " + fight.getWorld()); + + " on world " + fight.getWorld()); this.rowIdx = rowIdx; @@ -184,32 +184,51 @@ class FightLogDetailFrame extends JFrame praysUsedLine.add(defenderPrays, BorderLayout.EAST); - CombatLevels levels = fight.fightType.getCombatLevelsForType(); + //Pull levels from fight type or if they exist on attacker and defender + CombatLevels attackerLevels = fight.fightType.getCombatLevelsForType(); + CombatLevels defenderLevels = fight.fightType.getCombatLevelsForType(); + if(isCompetitorLog) { + if(log.getAttackerLevels() != null) { + attackerLevels = log.getAttackerLevels(); + } + if(log.getDefenderLevels() != null) { + defenderLevels = log.getDefenderLevels(); + } + } + if(!isCompetitorLog) { + if(log.getAttackerLevels() != null) { + attackerLevels = log.getDefenderLevels(); + } + if(log.getDefenderLevels() != null) { + defenderLevels = fight.competitor.getFightLogEntries().get(rowIdx).getAttackerLevels(); + } + } + combatLevelsLine = new JPanel(new BorderLayout()); JPanel attackerCombatLevels = new JPanel(new GridLayout(2, 3)); attackerAtkLvl = new JLabel(); PLUGIN.addSpriteToLabelIfValid(attackerAtkLvl, PvpPerformanceTrackerUtils.getSpriteForSkill(Skill.ATTACK)); - attackerAtkLvl.setText(String.valueOf(levels.atk)); + attackerAtkLvl.setText(String.valueOf(attackerLevels.atk)); attackerAtkLvl.setToolTipText("Attack Level"); attackerStrLvl = new JLabel(); PLUGIN.addSpriteToLabelIfValid(attackerStrLvl, PvpPerformanceTrackerUtils.getSpriteForSkill(Skill.STRENGTH)); - attackerStrLvl.setText(String.valueOf(levels.str)); + attackerStrLvl.setText(String.valueOf(attackerLevels.str)); attackerStrLvl.setToolTipText("Strength Level"); attackerDefLvl = new JLabel(); PLUGIN.addSpriteToLabelIfValid(attackerDefLvl, PvpPerformanceTrackerUtils.getSpriteForSkill(Skill.DEFENCE)); - attackerDefLvl.setText(String.valueOf(levels.def)); + attackerDefLvl.setText(String.valueOf(attackerLevels.def)); attackerDefLvl.setToolTipText("Defence Level"); attackerRangeLvl = new JLabel(); PLUGIN.addSpriteToLabelIfValid(attackerRangeLvl, PvpPerformanceTrackerUtils.getSpriteForSkill(Skill.RANGED)); - attackerRangeLvl.setText(String.valueOf(levels.range)); + attackerRangeLvl.setText(String.valueOf(attackerLevels.range)); attackerRangeLvl.setToolTipText("Ranged Level"); attackerMageLvl = new JLabel(); PLUGIN.addSpriteToLabelIfValid(attackerMageLvl, PvpPerformanceTrackerUtils.getSpriteForSkill(Skill.MAGIC)); - attackerMageLvl.setText(String.valueOf(levels.mage)); + attackerMageLvl.setText(String.valueOf(attackerLevels.mage)); attackerMageLvl.setToolTipText("Magic Level"); attackerHpLvl = new JLabel(); PLUGIN.addSpriteToLabelIfValid(attackerHpLvl, PvpPerformanceTrackerUtils.getSpriteForSkill(Skill.HITPOINTS)); - attackerHpLvl.setText(String.valueOf(levels.hp)); + attackerHpLvl.setText(String.valueOf(attackerLevels.hp)); attackerHpLvl.setToolTipText("Hitpoints Level"); attackerCombatLevels.add(attackerAtkLvl); @@ -222,28 +241,29 @@ class FightLogDetailFrame extends JFrame JPanel defenderCombatLevels = new JPanel(new GridLayout(2, 3)); defenderAtkLvl = new JLabel(); + PLUGIN.addSpriteToLabelIfValid(defenderAtkLvl, PvpPerformanceTrackerUtils.getSpriteForSkill(Skill.ATTACK)); - defenderAtkLvl.setText(String.valueOf(levels.atk)); + defenderAtkLvl.setText(String.valueOf(defenderLevels.atk)); defenderAtkLvl.setToolTipText("Attack Level"); defenderStrLvl = new JLabel(); PLUGIN.addSpriteToLabelIfValid(defenderStrLvl, PvpPerformanceTrackerUtils.getSpriteForSkill(Skill.STRENGTH)); - defenderStrLvl.setText(String.valueOf(levels.str)); + defenderStrLvl.setText(String.valueOf(defenderLevels.str)); defenderStrLvl.setToolTipText("Strength Level"); defenderDefLvl = new JLabel(); PLUGIN.addSpriteToLabelIfValid(defenderDefLvl, PvpPerformanceTrackerUtils.getSpriteForSkill(Skill.DEFENCE)); - defenderDefLvl.setText(String.valueOf(levels.def)); + defenderDefLvl.setText(String.valueOf(defenderLevels.def)); defenderDefLvl.setToolTipText("Defence Level"); defenderRangeLvl = new JLabel(); PLUGIN.addSpriteToLabelIfValid(defenderRangeLvl, PvpPerformanceTrackerUtils.getSpriteForSkill(Skill.RANGED)); - defenderRangeLvl.setText(String.valueOf(levels.range)); + defenderRangeLvl.setText(String.valueOf(defenderLevels.range)); defenderRangeLvl.setToolTipText("Ranged Level"); defenderMageLvl = new JLabel(); PLUGIN.addSpriteToLabelIfValid(defenderMageLvl, PvpPerformanceTrackerUtils.getSpriteForSkill(Skill.MAGIC)); - defenderMageLvl.setText(String.valueOf(levels.mage)); + defenderMageLvl.setText(String.valueOf(defenderLevels.mage)); defenderMageLvl.setToolTipText("Magic Level"); defenderHpLvl = new JLabel(); PLUGIN.addSpriteToLabelIfValid(defenderHpLvl, PvpPerformanceTrackerUtils.getSpriteForSkill(Skill.HITPOINTS)); - defenderHpLvl.setText(String.valueOf(levels.hp)); + defenderHpLvl.setText(String.valueOf(defenderLevels.hp)); defenderHpLvl.setToolTipText("Hitpoints Level"); defenderCombatLevels.add(defenderAtkLvl); @@ -259,12 +279,12 @@ class FightLogDetailFrame extends JFrame JPanel equipmentStatsLine = new JPanel(new BorderLayout()); JLabel attackerStatsLabel = new JLabel(); PLUGIN.getClientThread().invokeLater(() -> - attackerStatsLabel.setText(getItemEquipmentStatsString(log.getAttackerGear()))); + attackerStatsLabel.setText(getItemEquipmentStatsString(log.getAttackerGear()))); equipmentStatsLine.add(attackerStatsLabel, BorderLayout.WEST); JLabel defenderStatsLabel = new JLabel(); PLUGIN.getClientThread().invokeLater(() -> - defenderStatsLabel.setText(getItemEquipmentStatsString(log.getDefenderGear()))); + defenderStatsLabel.setText(getItemEquipmentStatsString(log.getDefenderGear()))); equipmentStatsLine.add(defenderStatsLabel, BorderLayout.EAST); JPanel equipmentRenderLine = new JPanel(new BorderLayout()); @@ -278,8 +298,8 @@ class FightLogDetailFrame extends JFrame JLabel attackerAnimationDetected = new JLabel(); attackerAnimationDetected.setText("Animation Detected: " + log.getAnimationData().toString() + ""); attackerAnimationDetected.setToolTipText("Note that the animation can be misleading, as many animations are re-used, but this is normal.
" + - "For example, Zammy Hasta and Staff of Fire use the same crush animation.
" + - "These were not intended to ever be displayed, but why not include them here."); + "For example, Zammy Hasta and Staff of Fire use the same crush animation.
" + + "These were not intended to ever be displayed, but why not include them here."); animationDetectedLine.add(attackerAnimationDetected, BorderLayout.CENTER); @@ -419,22 +439,22 @@ String getItemEquipmentStatsString(int[] equipment) } String sep = "
  "; return "Attack bonus" + sep + - "Stab: " + prependPlusIfPositive(stats.getAstab()) + sep + - "Slash: " + prependPlusIfPositive(stats.getAslash()) + sep + - "Crush: " + prependPlusIfPositive(stats.getAcrush()) + sep + - "Magic: " + prependPlusIfPositive(stats.getAmagic()) + sep + - "Range: " + prependPlusIfPositive(stats.getArange()) + - "
Defence bonus" + sep + - "Stab: " + prependPlusIfPositive(stats.getDstab()) + sep + - "Slash: " + prependPlusIfPositive(stats.getDslash()) + sep + - "Crush: " + prependPlusIfPositive(stats.getDcrush()) + sep + - "Magic: " + prependPlusIfPositive(stats.getDmagic()) + sep + - "Range: " + prependPlusIfPositive(stats.getDrange()) + - "
Other bonuses" + sep + - "Melee strength: " + prependPlusIfPositive(stats.getStr()) + sep + - "Ranged strength: " + prependPlusIfPositive(stats.getRstr() + ammoRangeStr) + sep + - "Magic damage: " + prependPlusIfPositive((int)stats.getMdmg()) + "%" + sep + - ""; + "Stab: " + prependPlusIfPositive(stats.getAstab()) + sep + + "Slash: " + prependPlusIfPositive(stats.getAslash()) + sep + + "Crush: " + prependPlusIfPositive(stats.getAcrush()) + sep + + "Magic: " + prependPlusIfPositive(stats.getAmagic()) + sep + + "Range: " + prependPlusIfPositive(stats.getArange()) + + "
Defence bonus" + sep + + "Stab: " + prependPlusIfPositive(stats.getDstab()) + sep + + "Slash: " + prependPlusIfPositive(stats.getDslash()) + sep + + "Crush: " + prependPlusIfPositive(stats.getDcrush()) + sep + + "Magic: " + prependPlusIfPositive(stats.getDmagic()) + sep + + "Range: " + prependPlusIfPositive(stats.getDrange()) + + "
Other bonuses" + sep + + "Melee strength: " + prependPlusIfPositive(stats.getStr()) + sep + + "Ranged strength: " + prependPlusIfPositive(stats.getRstr() + ammoRangeStr) + sep + + "Magic damage: " + prependPlusIfPositive((int)stats.getMdmg()) + "%" + sep + + ""; } String prependPlusIfPositive(int number)