Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
});
}
Expand Down Expand Up @@ -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(() -> {
Expand Down Expand Up @@ -1634,6 +1640,7 @@ private void attachPvpHubSyncedFight(FightPerformance fight)
try
{
fight.setPvpHubSyncedFight(syncedFight);
fight.getPvpHubDisplayFight().recalculateDharokAttacks();
}
catch (Exception e)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -118,6 +119,7 @@ public class FightPerformance implements Comparable<FightPerformance>
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.

Expand Down Expand Up @@ -217,6 +219,9 @@ public void checkForAttackAnimations(
animationData,
offensivePray,
competitorLevels,
null,
null,
getFighterMaxHp(competitor),
animationTick,
animationTime);
lastFightTime = animationTime;
Expand All @@ -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);
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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)
{
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Loading