diff --git a/src/main/java/matsyir/pvpperformancetracker/PvpPerformanceTrackerPlugin.java b/src/main/java/matsyir/pvpperformancetracker/PvpPerformanceTrackerPlugin.java index bed2f9d..59be675 100644 --- a/src/main/java/matsyir/pvpperformancetracker/PvpPerformanceTrackerPlugin.java +++ b/src/main/java/matsyir/pvpperformancetracker/PvpPerformanceTrackerPlugin.java @@ -1245,6 +1245,11 @@ else if (isDarkBow) // Update HP for the next iteration currentHp = hpAfterCurrent; } + + if (Objects.equals(attackerName, currentFight.getCompetitor().getName()) && currentHp != null) + { + currentFight.updateOpponentEstimatedHp(currentHp); + } }); }); } @@ -1557,11 +1562,12 @@ public void onSynced(FightPerformance syncedFight, String responseJson) { try { + fight.getPvpHubDisplayFight().recalculateDharokAttacks(); fight.getPvpHubDisplayFight().calculateRobeHits(config.robeHitFilter()); } catch (Exception e) { - log.warn("Error recalculating synced PvP-Hub robe hits {}: {}: {}", fight.getFightId(), e.getClass().getSimpleName(), e.getMessage()); + log.warn("Error recalculating synced PvP-Hub fight {}: {}: {}", fight.getFightId(), e.getClass().getSimpleName(), e.getMessage()); } SwingUtilities.invokeLater(() -> { @@ -1634,6 +1640,7 @@ private void attachPvpHubSyncedFight(FightPerformance fight) try { fight.setPvpHubSyncedFight(syncedFight); + fight.getPvpHubDisplayFight().recalculateDharokAttacks(); } catch (Exception e) { diff --git a/src/main/java/matsyir/pvpperformancetracker/controllers/FightPerformance.java b/src/main/java/matsyir/pvpperformancetracker/controllers/FightPerformance.java index 5d0d599..2d6471b 100644 --- a/src/main/java/matsyir/pvpperformancetracker/controllers/FightPerformance.java +++ b/src/main/java/matsyir/pvpperformancetracker/controllers/FightPerformance.java @@ -55,6 +55,7 @@ import net.runelite.api.Skill; import matsyir.pvpperformancetracker.PvpPerformanceTrackerConfig; import net.runelite.api.kit.KitType; +import static matsyir.pvpperformancetracker.utils.PvpUtils.calculateHpBeforeHit; import static matsyir.pvpperformancetracker.utils.PvpUtils.fixItemId; import static matsyir.pvpperformancetracker.controllers.PvpDamageCalc.RANGE_DEF; @@ -118,6 +119,7 @@ public class FightPerformance implements Comparable private transient int initialFightTick = -1; private transient boolean logTicksRelative = false; private transient int[] lastNonEmptyInventorySnapshot; + private transient Integer latestOpponentEstimatedHp; // Updated after matched local hits. private int competitorPrevHp; // intentionally don't serialize this, temp variable used to calculate hp healed. @@ -217,6 +219,9 @@ public void checkForAttackAnimations( animationData, offensivePray, competitorLevels, + null, + null, + getFighterMaxHp(competitor), animationTick, animationTime); lastFightTime = animationTime; @@ -234,7 +239,10 @@ else if (eName.equals(opponent.getName()) && Objects.equals(interactingName, com fightType, AssumedPrayers.localPrayerLevel(PLUGIN.getClient()), competitorLevels.def); - opponent.addAttack(competitor.getPlayer(), animationData, assumedOffensivePray, null, competitorLevels, animationTick, animationTime); + int opponentMaxHp = getAssumedOpponentMaxHp(); + int opponentHp = getOpponentHpAtAttack(eventSource, opponentMaxHp); + opponent.addAttack(competitor.getPlayer(), animationData, assumedOffensivePray, null, competitorLevels, + opponentHp, opponentMaxHp, animationTick, animationTime); 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(), animationTick, animationTime); @@ -423,6 +431,95 @@ public void setPvpHubSyncedFight(FightPerformance syncedFight) } } + public void updateOpponentEstimatedHp(Integer estimatedHp) + { + if (estimatedHp == null) + { + return; + } + latestOpponentEstimatedHp = Math.max(0, Math.min(estimatedHp, getAssumedOpponentMaxHp())); + } + + private int getOpponentHpAtAttack(Player player, int maxHp) + { + int ratio = player != null ? player.getHealthRatio() : -1; + int scale = player != null ? player.getHealthScale() : -1; + return estimateOpponentHpAtAttack(ratio, scale, latestOpponentEstimatedHp, maxHp); + } + + static int estimateOpponentHpAtAttack(int ratio, int scale, Integer latestMatchedHp, int maxHp) + { + maxHp = Math.max(1, maxHp); + int healthBarHp = calculateHpBeforeHit(ratio, scale, maxHp, 0); + int estimatedHp = healthBarHp >= 0 ? healthBarHp : latestMatchedHp != null ? latestMatchedHp : maxHp; + return Math.max(0, Math.min(estimatedHp, maxHp)); + } + + public String getDharokHpAtAttackText(FightLogEntry entry) + { + return entry == null ? "-" : entry.getDharokHpAtAttackText(getFighterMaxHp(getAttacker(entry))); + } + + public void recalculateDharokAttacks() + { + recalculateDharokAttacks(competitor); + recalculateDharokAttacks(opponent); + } + + private void recalculateDharokAttacks(Fighter fighter) + { + if (fighter == null || fighter.getFightLogEntries() == null) + { + return; + } + + PvpDamageCalc damageCalc = null; + int maxHp = -1; + for (FightLogEntry entry : fighter.getFightLogEntries()) + { + if (!entry.isFullEntry() || entry.getAttackerLevels() == null || !PvpDamageCalc.isFullDharokSet(entry.getAttackerGear())) + { + continue; + } + + if (damageCalc == null) + { + maxHp = getFighterMaxHp(fighter); + damageCalc = new PvpDamageCalc(this); + } + + double previousExpectedDamage = entry.getExpectedDamage(); + damageCalc.updateDamageStats(entry, entry.getAttackerLevels().hp, maxHp); + entry.setExpectedDamage(damageCalc.getAverageHit()); + entry.setMinHit(damageCalc.getMinHit()); + entry.setMaxHit(damageCalc.getMaxHit()); + fighter.adjustExpectedDamage(entry.getExpectedDamage() - previousExpectedDamage); + } + } + + private Fighter getAttacker(FightLogEntry entry) + { + if (entry != null && competitor != null && Objects.equals(entry.getAttackerName(), competitor.getName())) + { + return competitor; + } + return opponent; + } + + private int getFighterMaxHp(Fighter fighter) + { + if (fighter != null && fighter.getBaseLevels() != null && fighter.getBaseLevels().hp > 0) + { + return fighter.getBaseLevels().hp; + } + return fighter == opponent ? getAssumedOpponentMaxHp() : 99; + } + + private int getAssumedOpponentMaxHp() + { + return fightType != null && fightType.isLmsFight() ? 99 : CONFIG.opponentHitpointsLevel(); + } + public void recordPvpHubUploadName(String uploadName) { pvpHubUploadName = normalizeName(uploadName) == null ? null : uploadName.trim(); diff --git a/src/main/java/matsyir/pvpperformancetracker/controllers/Fighter.java b/src/main/java/matsyir/pvpperformancetracker/controllers/Fighter.java index 38533f0..1f0dd34 100644 --- a/src/main/java/matsyir/pvpperformancetracker/controllers/Fighter.java +++ b/src/main/java/matsyir/pvpperformancetracker/controllers/Fighter.java @@ -182,11 +182,19 @@ public BrewState getBrewState(FightLogEntry fightLogEntry) // Levels can be null void addAttack(Player opponent, AnimationData animationData, int offensivePray, CombatLevels levels, int attackTick, long attackTime) { - addAttack(opponent, animationData, offensivePray, levels, null, attackTick, attackTime); + addAttack(opponent, animationData, offensivePray, levels, null, null, 99, attackTick, attackTime); } // Levels can be null when that player's current boosted/drained stats are not visible locally. void addAttack(Player opponent, AnimationData animationData, int offensivePray, CombatLevels attackerLevels, CombatLevels defenderLevels, int attackTick, long attackTime) + { + addAttack(opponent, animationData, offensivePray, attackerLevels, defenderLevels, null, 99, attackTick, attackTime); + } + + // Estimated HP is only used for observed non-local attacks. + // Exact combat levels remain unavailable until PvP-Hub sync. + void addAttack(Player opponent, AnimationData animationData, int offensivePray, CombatLevels attackerLevels, + CombatLevels defenderLevels, Integer estimatedAttackerHp, int attackerMaxHp, int attackTick, long attackTime) { int[] attackerItems = player.getPlayerComposition().getEquipmentIds(); @@ -245,7 +253,10 @@ else if (weapon == EquipmentData.DRAGON_CROSSBOW && staffMeleeReduction = hasStaffMeleeReduction(opponent); } - pvpDamageCalc.updateDamageStats(player, opponent, successful, animationData, offensivePray, attackerLevels, defenderLevels); + int attackerCurrentHp = attackerLevels != null ? attackerLevels.hp : + estimatedAttackerHp != null ? estimatedAttackerHp : attackerMaxHp; + pvpDamageCalc.updateDamageStats(player, opponent, successful, animationData, offensivePray, attackerLevels, + defenderLevels, attackerCurrentHp, attackerMaxHp); if (elyProc) { pvpDamageCalc.applyElysianReduction(); @@ -268,6 +279,7 @@ else if (weapon == EquipmentData.DRAGON_CROSSBOW && } FightLogEntry fightLogEntry = new FightLogEntry(player, opponent, pvpDamageCalc, offensivePray, attackerLevels, animationData, attackTick, attackTime); + fightLogEntry.setAttackerEstimatedHp(estimatedAttackerHp); fightLogEntry.setDefenderElyProc(elyProc); fightLogEntry.setDefenderSotdMeleeReductionProc(staffMeleeReduction); fightLogEntry.setGmaulSpecial(isGmaulSpec); @@ -283,6 +295,12 @@ else if (weapon == EquipmentData.DRAGON_CROSSBOW && pendingAttacks.add(fightLogEntry); } + // Keep the aggregate in sync when PvP-Hub supplies a different attack-time HP. + void adjustExpectedDamage(double delta) + { + expectedDamage += delta; + } + public void addGhostBarrage(boolean successful, Player opponent, AnimationData animationData, int offensivePray, CombatLevels attackerLevels) { int currentTick = PLUGIN.getClient().getTickCount(); diff --git a/src/main/java/matsyir/pvpperformancetracker/controllers/PvpDamageCalc.java b/src/main/java/matsyir/pvpperformancetracker/controllers/PvpDamageCalc.java index 05ac0f8..aaa21b9 100644 --- a/src/main/java/matsyir/pvpperformancetracker/controllers/PvpDamageCalc.java +++ b/src/main/java/matsyir/pvpperformancetracker/controllers/PvpDamageCalc.java @@ -192,6 +192,16 @@ public void updateDamageStats(Player attacker, Player defender, boolean success, // Levels can be null when the client cannot observe that side's boosted/drained stats. public void updateDamageStats(Player attacker, Player defender, boolean success, AnimationData animationData, int offensivePray, CombatLevels currentAttackerLevels, CombatLevels currentDefenderLevels) + { + int attackerMaxHp = getDefaultCombatLevels().hp; + int attackerCurrentHp = currentAttackerLevels != null ? currentAttackerLevels.hp : attackerMaxHp; + updateDamageStats(attacker, defender, success, animationData, offensivePray, currentAttackerLevels, currentDefenderLevels, + attackerCurrentHp, attackerMaxHp); + } + + // Dharok's current/max HP are passed separately so observed opponents can keep their combat levels unknown. + public void updateDamageStats(Player attacker, Player defender, boolean success, AnimationData animationData, int offensivePray, + CombatLevels currentAttackerLevels, CombatLevels currentDefenderLevels, int attackerCurrentHp, int attackerMaxHp) { // shouldn't be possible, but just in case if (attacker == null || defender == null) { return; } @@ -232,6 +242,7 @@ public void updateDamageStats(Player attacker, Player defender, boolean success, if (attackStyle.isMelee() || animationData == AnimationData.MELEE_VOIDWAKER_SPEC) { getMeleeMaxHit(playerStats[STRENGTH_BONUS], isSpecial, weapon, voidStyle, offensivePray); + applyDharokSetEffect(attackerItems, attackerCurrentHp, attackerMaxHp); getMeleeAccuracy(playerStats, opponentStats, attackStyle, isSpecial, weapon, voidStyle, offensivePray, defencePrayerModifier); } else if (attackStyle == AttackStyle.RANGED) @@ -266,9 +277,20 @@ else if (attackStyle == AttackStyle.MAGIC) // secondary function used to analyze fights from the fight log (fight analysis/fight merge) public void updateDamageStats(FightLogEntry atkLog, FightLogEntry defenderLog) + { + updateDamageStats(atkLog, defenderLog, null, 0); + } + + void updateDamageStats(FightLogEntry atkLog, int attackerCurrentHp, int attackerMaxHp) + { + updateDamageStats(atkLog, null, attackerCurrentHp, attackerMaxHp); + } + + private void updateDamageStats(FightLogEntry atkLog, FightLogEntry defenderLog, Integer attackerCurrentHp, int attackerMaxHp) { this.attackerLevels = atkLog.getAttackerLevels() != null ? atkLog.getAttackerLevels() : getDefaultCombatLevels(); - this.defenderLevels = defenderLog.getAttackerLevels() != null ? defenderLog.getAttackerLevels() : getDefaultCombatLevels(); + this.defenderLevels = defenderLog != null && defenderLog.getAttackerLevels() != null ? + defenderLog.getAttackerLevels() : getDefaultCombatLevels(); int[] attackerItems = atkLog.getAttackerGear(); int[] defenderItems = atkLog.getDefenderGear(); boolean success = atkLog.success(); @@ -286,7 +308,8 @@ public void updateDamageStats(FightLogEntry atkLog, FightLogEntry defenderLog) EquipmentData weapon = EquipmentData.fromId(fixItemId(attackerItems[KitType.WEAPON.getIndex()])); - int[] playerStats = this.calculateBonuses(attackerItems); + RingData loggedRing = atkLog.getAttackerRingItemId() != null ? RingData.fromId(atkLog.getAttackerRingItemId()) : ringUsed; + int[] playerStats = attackerCurrentHp != null ? calculateBonuses(attackerItems, loggedRing) : this.calculateBonuses(attackerItems); int[] opponentStats = this.calculateBonuses(defenderItems); AnimationData.AttackStyle attackStyle = animationData.attackStyle; // basic style: stab/slash/crush/ranged/magic Integer attackerAmmoItemId = atkLog.getAttackerAmmoItemId(); @@ -300,7 +323,15 @@ public void updateDamageStats(FightLogEntry atkLog, FightLogEntry defenderLog) if (attackStyle.isMelee()) { getMeleeMaxHit(playerStats[STRENGTH_BONUS], isSpecial, weapon, voidStyle, offensivePray); - getMeleeAccuracy(playerStats, opponentStats, attackStyle, isSpecial, weapon, voidStyle, offensivePray, PIETY_DEF_PRAYER_MODIFIER); + if (attackerCurrentHp != null) + { + applyDharokSetEffect(attackerItems, attackerCurrentHp, attackerMaxHp); + accuracy = atkLog.getAccuracy(); + } + else + { + getMeleeAccuracy(playerStats, opponentStats, attackStyle, isSpecial, weapon, voidStyle, offensivePray, PIETY_DEF_PRAYER_MODIFIER); + } } else if (attackStyle == AttackStyle.RANGED) { @@ -338,6 +369,40 @@ else if (attackStyle == AttackStyle.MAGIC) } } + public static boolean isFullDharokSet(int[] equipment) + { + int lastRequiredIndex = Math.max(Math.max(KitType.HEAD.getIndex(), KitType.WEAPON.getIndex()), + Math.max(KitType.TORSO.getIndex(), KitType.LEGS.getIndex())); + if (equipment == null || equipment.length <= lastRequiredIndex) + { + return false; + } + + return EquipmentData.fromId(fixItemId(equipment[KitType.HEAD.getIndex()])) == EquipmentData.DHAROKS_HELM && + EquipmentData.fromId(fixItemId(equipment[KitType.WEAPON.getIndex()])) == EquipmentData.DHAROKS_GREATAXE && + EquipmentData.fromId(fixItemId(equipment[KitType.TORSO.getIndex()])) == EquipmentData.DHAROKS_PLATEBODY && + EquipmentData.fromId(fixItemId(equipment[KitType.LEGS.getIndex()])) == EquipmentData.DHAROKS_PLATELEGS; + } + + static int scaleDharokMaxHit(int baseMaxHit, int currentHp, int maxHp) + { + baseMaxHit = Math.max(0, baseMaxHit); + maxHp = Math.max(1, maxHp); + int missingHp = Math.max(0, maxHp - Math.max(0, currentHp)); + long modifierNumerator = 10000L + (long) missingHp * maxHp; + return (int) ((baseMaxHit * modifierNumerator) / 10000L); + } + + private void applyDharokSetEffect(int[] equipment, int currentHp, int maxHp) + { + if (!isFullDharokSet(equipment)) + { + return; + } + + maxHit = scaleDharokMaxHit(maxHit, currentHp, maxHp); + } + private CombatLevels getDefaultCombatLevels() { return defaultCombatLevels != null ? defaultCombatLevels : CombatLevels.getConfigLevels(); diff --git a/src/main/java/matsyir/pvpperformancetracker/controllers/PvpHubUploader.java b/src/main/java/matsyir/pvpperformancetracker/controllers/PvpHubUploader.java index 514ce2f..fec47c3 100644 --- a/src/main/java/matsyir/pvpperformancetracker/controllers/PvpHubUploader.java +++ b/src/main/java/matsyir/pvpperformancetracker/controllers/PvpHubUploader.java @@ -26,6 +26,7 @@ package matsyir.pvpperformancetracker.controllers; import com.google.gson.Gson; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; @@ -167,6 +168,8 @@ static String serializeFightUpload(FightPerformance fight, Gson gson, int public static String serializeFightUpload(FightPerformance fight, Gson gson, int publicDelaySeconds, String hiddenName) { JsonObject payload = gson.toJsonTree(new FightUploadPayload(fight, publicDelaySeconds)).getAsJsonObject(); + removeAttackerEstimatedHp(payload.getAsJsonObject("c")); + removeAttackerEstimatedHp(payload.getAsJsonObject("o")); if (hiddenName != null && !hiddenName.trim().isEmpty()) { payload.getAsJsonObject("c").addProperty("n", hiddenName); @@ -175,6 +178,22 @@ static String serializeFightUpload(FightPerformance fight, Gson gson, int public return gson.toJson(payload); } + private static void removeAttackerEstimatedHp(JsonObject fighter) + { + if (fighter == null || !fighter.has("l") || !fighter.get("l").isJsonArray()) + { + return; + } + + for (JsonElement entry : fighter.getAsJsonArray("l")) + { + if (entry.isJsonObject()) + { + entry.getAsJsonObject().remove("aH"); + } + } + } + static final class FightUploadPayload { @Expose diff --git a/src/main/java/matsyir/pvpperformancetracker/models/EquipmentData.java b/src/main/java/matsyir/pvpperformancetracker/models/EquipmentData.java index 2a10192..0d41547 100644 --- a/src/main/java/matsyir/pvpperformancetracker/models/EquipmentData.java +++ b/src/main/java/matsyir/pvpperformancetracker/models/EquipmentData.java @@ -80,8 +80,10 @@ public enum EquipmentData AMULET_OF_FURY(ItemID.AMULET_OF_FURY, ItemID.AMULET_OF_FURY_23640), BANDOS_TASSETS(ItemID.BANDOS_TASSETS, ItemID.BANDOS_TASSETS_23646), BLESSED_SPIRIT_SHIELD(ItemID.BLESSED_SPIRIT_SHIELD, ItemID.BLESSED_SPIRIT_SHIELD_23642), - DHAROKS_HELM(ItemID.DHAROKS_HELM, ItemID.DHAROKS_HELM_23639), - DHAROKS_PLATELEGS(ItemID.DHAROKS_PLATELEGS, ItemID.DHAROKS_PLATELEGS_23633), + DHAROKS_HELM(ItemID.DHAROKS_HELM, ItemID.DHAROKS_HELM_100, ItemID.DHAROKS_HELM_75, ItemID.DHAROKS_HELM_50, ItemID.DHAROKS_HELM_25, ItemID.DHAROKS_HELM_23639), + DHAROKS_GREATAXE(ItemID.DHAROKS_GREATAXE, ItemID.DHAROKS_GREATAXE_100, ItemID.DHAROKS_GREATAXE_75, ItemID.DHAROKS_GREATAXE_50, ItemID.DHAROKS_GREATAXE_25, ItemID.DHAROKS_GREATAXE_25516), + DHAROKS_PLATEBODY(ItemID.DHAROKS_PLATEBODY, ItemID.DHAROKS_PLATEBODY_100, ItemID.DHAROKS_PLATEBODY_75, ItemID.DHAROKS_PLATEBODY_50, ItemID.DHAROKS_PLATEBODY_25, ItemID.DHAROKS_PLATEBODY_25515), + DHAROKS_PLATELEGS(ItemID.DHAROKS_PLATELEGS, ItemID.DHAROKS_PLATELEGS_100, ItemID.DHAROKS_PLATELEGS_75, ItemID.DHAROKS_PLATELEGS_50, ItemID.DHAROKS_PLATELEGS_25, ItemID.DHAROKS_PLATELEGS_23633), GUTHANS_HELM(ItemID.GUTHANS_HELM, ItemID.GUTHANS_HELM_23638), KARILS_TOP(ItemID.KARILS_LEATHERTOP, ItemID.KARILS_LEATHERTOP_23632), TORAGS_HELM(ItemID.TORAGS_HELM, ItemID.TORAGS_HELM_23637), diff --git a/src/main/java/matsyir/pvpperformancetracker/models/FightLogEntry.java b/src/main/java/matsyir/pvpperformancetracker/models/FightLogEntry.java index e097f4c..e4459ec 100644 --- a/src/main/java/matsyir/pvpperformancetracker/models/FightLogEntry.java +++ b/src/main/java/matsyir/pvpperformancetracker/models/FightLogEntry.java @@ -110,6 +110,10 @@ public class FightLogEntry implements Comparable @SerializedName("C") @Setter private CombatLevels attackerLevels; // CAN BE NULL + @Expose + @SerializedName("aH") + @Setter + private Integer attackerEstimatedHp; // Used only when the attacker's exact C.h is unavailable. @Getter @Setter @@ -430,6 +434,25 @@ public String getHitRange() return minHit + "-" + maxHit; } + public String getDharokHpAtAttackText(int maxHp) + { + if (!PvpDamageCalc.isFullDharokSet(attackerGear)) + { + return "-"; + } + + maxHp = Math.max(1, maxHp); + if (attackerLevels != null) + { + return attackerLevels.hp + "/" + maxHp; + } + if (attackerEstimatedHp != null) + { + return "~" + attackerEstimatedHp + "/" + maxHp; + } + return "-"; + } + // use to sort by last fight time, to sort fights by date/time. @Override public int compareTo(FightLogEntry o) diff --git a/src/main/java/matsyir/pvpperformancetracker/views/FightLogDetailFrame.java b/src/main/java/matsyir/pvpperformancetracker/views/FightLogDetailFrame.java index 13b79f0..7e12467 100644 --- a/src/main/java/matsyir/pvpperformancetracker/views/FightLogDetailFrame.java +++ b/src/main/java/matsyir/pvpperformancetracker/views/FightLogDetailFrame.java @@ -178,6 +178,8 @@ class FightLogDetailFrame extends JFrame defenderName.setText("Defender:
" + defender.getName() + ""); namesLine.add(defenderName, BorderLayout.EAST); + boolean isDharokAttack = PvpDamageCalc.isFullDharokSet(logEntry.getAttackerGear()); + // prayer display line JPanel praysUsedLine = new JPanel(new BorderLayout()); GridLayout attackerPrayLayout = new GridLayout(1, 2); @@ -249,8 +251,9 @@ class FightLogDetailFrame extends JFrame attackerMageLvl.setToolTipText("Magic Level"); attackerHpLvl = new JLabel(); PLUGIN.addSpriteToLabelIfValid(attackerHpLvl, PvpUtils.getSpriteForSkill(Skill.HITPOINTS)); - attackerHpLvl.setText(String.valueOf(atkLevels.hp)); - attackerHpLvl.setToolTipText("Hitpoints Level"); + attackerHpLvl.setText(isDharokAttack ? fight.getDharokHpAtAttackText(logEntry) : String.valueOf(atkLevels.hp)); + attackerHpLvl.setToolTipText(isDharokAttack ? + "Hitpoints at attack. A leading ~ means the value was estimated locally." : "Hitpoints Level"); attackerCombatLevels.add(attackerAtkLvl); attackerCombatLevels.add(attackerStrLvl); diff --git a/src/test/java/matsyir/pvpperformancetracker/controllers/FightPerformancePvpHubSyncTest.java b/src/test/java/matsyir/pvpperformancetracker/controllers/FightPerformancePvpHubSyncTest.java index 078653c..dd387ae 100644 --- a/src/test/java/matsyir/pvpperformancetracker/controllers/FightPerformancePvpHubSyncTest.java +++ b/src/test/java/matsyir/pvpperformancetracker/controllers/FightPerformancePvpHubSyncTest.java @@ -97,6 +97,15 @@ public void syncedFightDoesNotUseAmbiguousStatsToSwap() assertEquals(50, displayFight.getOpponent().getDamageDealt()); } + @Test + public void attackTimeOpponentHpPrefersVisibleHealthBarThenMatchedHit() + { + assertEquals(50, FightPerformance.estimateOpponentHpAtAttack(15, 30, 20, 99)); + assertEquals(20, FightPerformance.estimateOpponentHpAtAttack(-1, -1, 20, 99)); + assertEquals(99, FightPerformance.estimateOpponentHpAtAttack(-1, -1, null, 99)); + assertEquals(0, FightPerformance.estimateOpponentHpAtAttack(0, 30, 20, 99)); + } + private static FightPerformance fight(String competitorName, String opponentName, int competitorDamage, int opponentDamage) { FightPerformance fight = new FightPerformance(); diff --git a/src/test/java/matsyir/pvpperformancetracker/controllers/PvpDamageCalcDharokTest.java b/src/test/java/matsyir/pvpperformancetracker/controllers/PvpDamageCalcDharokTest.java new file mode 100644 index 0000000..d0a6dfe --- /dev/null +++ b/src/test/java/matsyir/pvpperformancetracker/controllers/PvpDamageCalcDharokTest.java @@ -0,0 +1,87 @@ +package matsyir.pvpperformancetracker.controllers; + +import net.runelite.api.ItemID; +import net.runelite.api.PlayerComposition; +import net.runelite.api.kit.KitType; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class PvpDamageCalcDharokTest +{ + @Test + public void detectsNormalMixedDegradedAndLmsSets() + { + assertTrue(PvpDamageCalc.isFullDharokSet(dharokEquipment( + ItemID.DHAROKS_HELM, + ItemID.DHAROKS_GREATAXE, + ItemID.DHAROKS_PLATEBODY, + ItemID.DHAROKS_PLATELEGS))); + assertTrue(PvpDamageCalc.isFullDharokSet(dharokEquipment( + ItemID.DHAROKS_HELM_75, + ItemID.DHAROKS_GREATAXE_25, + ItemID.DHAROKS_PLATEBODY_100, + ItemID.DHAROKS_PLATELEGS_50))); + assertTrue(PvpDamageCalc.isFullDharokSet(dharokEquipment( + ItemID.DHAROKS_HELM_23639, + ItemID.DHAROKS_GREATAXE_25516, + ItemID.DHAROKS_PLATEBODY_25515, + ItemID.DHAROKS_PLATELEGS_23633))); + } + + @Test + public void excludesBrokenAndIncompleteSets() + { + assertFalse(PvpDamageCalc.isFullDharokSet(dharokEquipment( + ItemID.DHAROKS_HELM_0, + ItemID.DHAROKS_GREATAXE, + ItemID.DHAROKS_PLATEBODY, + ItemID.DHAROKS_PLATELEGS))); + assertFalse(PvpDamageCalc.isFullDharokSet(dharokEquipment( + ItemID.DHAROKS_HELM, + ItemID.DHAROKS_GREATAXE_0, + ItemID.DHAROKS_PLATEBODY, + ItemID.DHAROKS_PLATELEGS))); + assertFalse(PvpDamageCalc.isFullDharokSet(dharokEquipment( + ItemID.DHAROKS_HELM, + ItemID.DHAROKS_GREATAXE, + ItemID.DHAROKS_PLATEBODY_0, + ItemID.DHAROKS_PLATELEGS))); + assertFalse(PvpDamageCalc.isFullDharokSet(dharokEquipment( + ItemID.DHAROKS_HELM, + ItemID.DHAROKS_GREATAXE, + ItemID.DHAROKS_PLATEBODY, + ItemID.DHAROKS_PLATELEGS_0))); + + int[] missingBody = dharokEquipment( + ItemID.DHAROKS_HELM, + ItemID.DHAROKS_GREATAXE, + ItemID.DHAROKS_PLATEBODY, + ItemID.DHAROKS_PLATELEGS); + missingBody[KitType.TORSO.getIndex()] = 0; + assertFalse(PvpDamageCalc.isFullDharokSet(missingBody)); + assertFalse(PvpDamageCalc.isFullDharokSet(new int[1])); + } + + @Test + public void scalesDharokMaxHitAndDoesNotPenalizeOverhealing() + { + assertEquals(50, PvpDamageCalc.scaleDharokMaxHit(50, 99, 99)); + assertEquals(74, PvpDamageCalc.scaleDharokMaxHit(50, 50, 99)); + assertEquals(98, PvpDamageCalc.scaleDharokMaxHit(50, 1, 99)); + assertEquals(50, PvpDamageCalc.scaleDharokMaxHit(50, 120, 99)); + } + + private static int[] dharokEquipment(int helm, int axe, int body, int legs) + { + int[] equipment = new int[12]; + equipment[KitType.HEAD.getIndex()] = helm + PlayerComposition.ITEM_OFFSET; + equipment[KitType.WEAPON.getIndex()] = axe + PlayerComposition.ITEM_OFFSET; + equipment[KitType.TORSO.getIndex()] = body + PlayerComposition.ITEM_OFFSET; + equipment[KitType.LEGS.getIndex()] = legs + PlayerComposition.ITEM_OFFSET; + return equipment; + } + +} diff --git a/src/test/java/matsyir/pvpperformancetracker/controllers/PvpHubUploaderTest.java b/src/test/java/matsyir/pvpperformancetracker/controllers/PvpHubUploaderTest.java new file mode 100644 index 0000000..2bd494e --- /dev/null +++ b/src/test/java/matsyir/pvpperformancetracker/controllers/PvpHubUploaderTest.java @@ -0,0 +1,38 @@ +package matsyir.pvpperformancetracker.controllers; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import matsyir.pvpperformancetracker.PvpPerformanceTrackerConfig; +import matsyir.pvpperformancetracker.PvpPerformanceTrackerPlugin; +import matsyir.pvpperformancetracker.models.FightLogEntry; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class PvpHubUploaderTest +{ + private static final Gson GSON = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + + @Test + public void uploadOmitsEstimatedAttackerHpWithoutChangingSavedFightJson() + { + PvpPerformanceTrackerPlugin.CONFIG = new PvpPerformanceTrackerConfig() + { + }; + FightLogEntry entry = new FightLogEntry(new int[12], 25, 0.75, 0, 50, new int[12], "opponent"); + entry.setAttackerEstimatedHp(42); + + FightPerformance fight = new FightPerformance(); + fight.competitor = new Fighter("competitor"); + fight.opponent = new Fighter("opponent"); + fight.opponent.getFightLogEntries().add(entry); + + JsonObject upload = GSON.fromJson(PvpHubUploader.serializeFightUpload(fight, GSON, 0), JsonObject.class); + JsonObject uploadedEntry = upload.getAsJsonObject("o").getAsJsonArray("l").get(0).getAsJsonObject(); + + assertFalse(uploadedEntry.has("aH")); + assertTrue(GSON.toJson(fight).contains("\"aH\":42")); + } +} diff --git a/src/test/java/matsyir/pvpperformancetracker/models/FightLogEntryDharokHpTest.java b/src/test/java/matsyir/pvpperformancetracker/models/FightLogEntryDharokHpTest.java new file mode 100644 index 0000000..a16c92c --- /dev/null +++ b/src/test/java/matsyir/pvpperformancetracker/models/FightLogEntryDharokHpTest.java @@ -0,0 +1,62 @@ +package matsyir.pvpperformancetracker.models; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import net.runelite.api.ItemID; +import net.runelite.api.PlayerComposition; +import net.runelite.api.kit.KitType; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class FightLogEntryDharokHpTest +{ + private static final Gson GSON = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + + @Test + public void formatsExactEstimatedFallbackAndLegacyHp() + { + FightLogEntry exact = entry(); + exact.setAttackerLevels(new CombatLevels(99, 118, 99, 99, 99, 37)); + assertEquals("37/99", exact.getDharokHpAtAttackText(99)); + + FightLogEntry estimated = entry(); + estimated.setAttackerEstimatedHp(37); + assertEquals("~37/99", estimated.getDharokHpAtAttackText(99)); + + FightLogEntry fallback = entry(); + fallback.setAttackerEstimatedHp(99); + assertEquals("~99/99", fallback.getDharokHpAtAttackText(99)); + + assertEquals("-", entry().getDharokHpAtAttackText(99)); + } + + @Test + public void estimatedHpIsAdditiveAndLegacyJsonStillLoads() + { + FightLogEntry entry = entry(); + entry.setAttackerEstimatedHp(42); + + String json = GSON.toJson(entry); + assertTrue(json.contains("\"aH\":42")); + + JsonObject legacyJson = GSON.fromJson(json, JsonObject.class); + legacyJson.remove("aH"); + FightLogEntry legacyEntry = GSON.fromJson(legacyJson, FightLogEntry.class); + assertNull(legacyEntry.getAttackerEstimatedHp()); + assertEquals("-", legacyEntry.getDharokHpAtAttackText(99)); + } + + private static FightLogEntry entry() + { + int[] equipment = new int[12]; + equipment[KitType.HEAD.getIndex()] = ItemID.DHAROKS_HELM + PlayerComposition.ITEM_OFFSET; + equipment[KitType.WEAPON.getIndex()] = ItemID.DHAROKS_GREATAXE + PlayerComposition.ITEM_OFFSET; + equipment[KitType.TORSO.getIndex()] = ItemID.DHAROKS_PLATEBODY + PlayerComposition.ITEM_OFFSET; + equipment[KitType.LEGS.getIndex()] = ItemID.DHAROKS_PLATELEGS + PlayerComposition.ITEM_OFFSET; + return new FightLogEntry(equipment, 25, 0.75, 0, 50, new int[12], "attacker"); + } +}