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 @@ -63,6 +63,7 @@
import lombok.extern.slf4j.Slf4j;
import matsyir.pvpperformancetracker.controllers.FightPerformance;
import matsyir.pvpperformancetracker.controllers.Fighter;
import matsyir.pvpperformancetracker.models.AnimationData;
import matsyir.pvpperformancetracker.models.CombatLevels;
import matsyir.pvpperformancetracker.models.FightLogEntry;
import matsyir.pvpperformancetracker.models.HitsplatInfo;
Expand Down Expand Up @@ -204,6 +205,7 @@ public class PvpPerformanceTrackerPlugin extends Plugin
// do not cache items in the same way since we could potentially cache a very large amount of them.
private final Map<Integer, List<HitsplatInfo>> hitsplatBuffer = new HashMap<>();
private final Map<Integer, List<HitsplatInfo>> incomingHitsplatsBuffer = new ConcurrentHashMap<>(); // Stores hitsplats *received* by players per tick.
private final Map<String, Integer> lastNonGmaulSpecTickByAttacker = new ConcurrentHashMap<>();
private HiscoreEndpoint hiscoreEndpoint = HiscoreEndpoint.NORMAL; // Added field

// #################################################################################################################
Expand Down Expand Up @@ -857,6 +859,27 @@ else if (opponentIsTrackedCompetitor)
// Gmaul can hit twice, others match expected hits
int hitsToFind = entry.isGmaulSpecial() ? 2 : toMatch;

// Enforce Dragon Claws 2+2 sequencing: limit phase one to two hits
if (entry.getAnimationData() == AnimationData.MELEE_DRAGON_CLAWS_SPEC && entry.getMatchedHitsCount() < 2)
{
int remainingPhase1 = Math.max(0, 2 - entry.getMatchedHitsCount());
hitsToFind = Math.min(hitsToFind, remainingPhase1);
}

// Simple double-GMaul gate: if a different special fired on the previous tick, cap to a single hit
if (entry.isGmaulSpecial())
{
String attackerName = attacker.getName();
if (attackerName != null)
{
Integer lastSpec = lastNonGmaulSpecTickByAttacker.get(attackerName);
if (lastSpec != null && lastSpec == entry.getTick() - 1)
{
hitsToFind = Math.min(hitsToFind, 1);
}
}
}

while (matchedThisCycle < hitsToFind && hitsIter.hasNext())
{
HitsplatInfo hInfo = hitsIter.next();
Expand Down Expand Up @@ -894,15 +917,32 @@ else if (opponentIsTrackedCompetitor)
// Fallback to current ratio/scale if polled is unavailable
if (ratio < 0 || scale <= 0) { ratio = opponent.getHealthRatio(); scale = opponent.getHealthScale(); }
int hpBefore = -1;
int hpBeforeThisCycle = -1;
if (ratio >= 0 && scale > 0 && maxHpToUse > 0)
{
hpBefore = PvpPerformanceTrackerUtils.calculateHpBeforeHit(ratio, scale, maxHpToUse, entry.getActualDamageSum());
hpBeforeThisCycle = PvpPerformanceTrackerUtils.calculateHpBeforeHit(ratio, scale, maxHpToUse, damageThisCycle);
}
if (hpBefore > 0)
{
entry.setEstimatedHpBeforeHit(hpBefore);
entry.setOpponentMaxHp(maxHpToUse);
}

if (entry.getAnimationData() == AnimationData.MELEE_DRAGON_CLAWS_SPEC)
{
int matched = entry.getMatchedHitsCount();
if (matched == 2 && entry.getClawsHpBeforePhase1() == null && hpBeforeThisCycle > 0)
{
entry.setClawsHpBeforePhase1(hpBeforeThisCycle);
entry.setClawsPhase1Damage(damageThisCycle);
entry.setClawsHpAfterPhase1(hpBeforeThisCycle - damageThisCycle);
}
if (matched >= entry.getExpectedHits() && entry.getClawsHpBeforePhase2() == null && hpBeforeThisCycle > 0)
{
entry.setClawsHpBeforePhase2(hpBeforeThisCycle);
}
}
}
}

Expand Down Expand Up @@ -990,9 +1030,34 @@ else if (opponentIsTrackedCompetitor)
entry.setDisplayHpBefore(hpBeforeCurrent);
entry.setDisplayHpAfter(hpAfterCurrent);

Double koChanceCurrent = (hpBeforeCurrent != null)
? PvpPerformanceTrackerUtils.calculateKoChance(entry.getAccuracy(), entry.getMinHit(), entry.getMaxHit(), hpBeforeCurrent)
: null;
Double koChanceCurrent = null;
boolean isClawsSpec = entry.getAnimationData() == AnimationData.MELEE_DRAGON_CLAWS_SPEC && entry.getExpectedHits() >= 4;
if (isClawsSpec)
{
if (hpBeforeCurrent != null && entry.getMatchedHitsCount() >= entry.getExpectedHits())
{
int healBetween = 0;
Integer hpAfterP1 = entry.getClawsHpAfterPhase1();
Integer hpBeforeP2 = entry.getClawsHpBeforePhase2();
if (hpAfterP1 != null && hpBeforeP2 != null)
{
healBetween = Math.max(0, hpBeforeP2 - hpAfterP1);
}
koChanceCurrent = PvpPerformanceTrackerUtils.calculateClawsTwoPhaseKo(entry.getAccuracy(), entry.getMaxHit(), hpBeforeCurrent, healBetween);
}
}
else
{
koChanceCurrent = (hpBeforeCurrent != null)
? PvpPerformanceTrackerUtils.calculateKoChance(entry.getAccuracy(), entry.getMinHit(), entry.getMaxHit(), hpBeforeCurrent)
: null;
}

if (koChanceCurrent != null && koChanceCurrent <= 0.0)
{
koChanceCurrent = null;
}

entry.setDisplayKoChance(koChanceCurrent);
entry.setKoChance(koChanceCurrent);

Expand Down Expand Up @@ -1030,6 +1095,15 @@ public void onPlayerDespawned(PlayerDespawned event)
}
}

public void recordNonGmaulSpecial(String attackerName, int tick)
{
if (attackerName == null)
{
return;
}
lastNonGmaulSpecTickByAttacker.put(attackerName, tick);
}

// #################################################################################################################
// ################################## Plugin-specific functions & global helpers ###################################
// #################################################################################################################
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,10 @@ else if (weapon == EquipmentData.DRAGON_CROSSBOW &&

FightLogEntry fightLogEntry = new FightLogEntry(player, opponent, pvpDamageCalc, offensivePray, levels, animationData);
fightLogEntry.setGmaulSpecial(isGmaulSpec);
if (animationData.isSpecial && animationData != AnimationData.MELEE_GRANITE_MAUL_SPEC)
{
PvpPerformanceTrackerPlugin.PLUGIN.recordNonGmaulSpecial(player.getName(), fightLogEntry.getTick());
}
if (PvpPerformanceTrackerPlugin.CONFIG.fightLogInChat())
{
PvpPerformanceTrackerPlugin.PLUGIN.sendTradeChatMessage(fightLogEntry.toChatMessage());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,52 @@ public class FightLogEntry implements Comparable<FightLogEntry>
@Getter @Setter
private boolean isPartOfTickGroup = false;

// Transient fields for handling multi-tick Dragon Claws special attacks
private transient Integer clawsPhase1Damage = null;
private transient Integer clawsHpBeforePhase1 = null;
private transient Integer clawsHpAfterPhase1 = null;
private transient Integer clawsHpBeforePhase2 = null;

public Integer getClawsPhase1Damage()
{
return clawsPhase1Damage;
}

public void setClawsPhase1Damage(Integer clawsPhase1Damage)
{
this.clawsPhase1Damage = clawsPhase1Damage;
}

public Integer getClawsHpBeforePhase1()
{
return clawsHpBeforePhase1;
}

public void setClawsHpBeforePhase1(Integer clawsHpBeforePhase1)
{
this.clawsHpBeforePhase1 = clawsHpBeforePhase1;
}

public Integer getClawsHpAfterPhase1()
{
return clawsHpAfterPhase1;
}

public void setClawsHpAfterPhase1(Integer clawsHpAfterPhase1)
{
this.clawsHpAfterPhase1 = clawsHpAfterPhase1;
}

public Integer getClawsHpBeforePhase2()
{
return clawsHpBeforePhase2;
}

public void setClawsHpBeforePhase2(Integer clawsHpBeforePhase2)
{
this.clawsHpBeforePhase2 = clawsHpBeforePhase2;
}

public FightLogEntry(Player attacker, Player defender, PvpDamageCalc pvpDamageCalc, int attackerOffensivePray, CombatLevels levels, AnimationData animationData)
{
this.isFullEntry = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,111 @@ public static int calculateHpBeforeHit(int ratio, int scale, int maxHp, int dama
return hpAfter + damageSum;
}

/**
* Attempt-level KO probability for Dragon Claws specials across two ticks, factoring any healing between phases.
*/
public static Double calculateClawsTwoPhaseKo(double specAccuracy, int specMaxHit, int hpBefore, int healBetween)
{
if (specMaxHit <= 0 || hpBefore <= 0)
{
return null;
}

double accSpec = Math.max(0.0, Math.min(1.0, specAccuracy));
int baseMax = Math.max(0, (specMaxHit - 1) / 2);
if (baseMax <= 0)
{
return null;
}

double swingAccuracy;
if (accSpec <= 0.0)
{
swingAccuracy = 0.0;
}
else if (accSpec >= 1.0)
{
swingAccuracy = 1.0;
}
else
{
swingAccuracy = 1.0 - Math.pow(1.0 - accSpec, 0.25);
}

double missChance = 1.0 - swingAccuracy;
double p1 = swingAccuracy;
double p2 = missChance * swingAccuracy;
double p3 = missChance * missChance * swingAccuracy;
double p4 = missChance * missChance * missChance * swingAccuracy;

double inverseCount = 1.0 / (baseMax + 1);
double ko = 0.0;

for (int roll = 0; roll <= baseMax; roll++)
{
int halfCeil = (roll + 1) / 2;
int halfFloor = roll / 2;
int quarterFloor = roll / 4;
int threeQuarterCeil = (int) Math.ceil(0.75 * roll);
int threeQuarterFloor = (int) Math.floor(0.75 * roll);

// Case E1: first swing connects (two hits tick k, two hits tick k+1)
{
int h1 = roll;
int h2 = halfCeil;
int h3 = quarterFloor;
int used = h1 + h2 + h3;
int remainder = Math.max(0, 2 * roll - used);
int damageTick1 = h1 + h2;
int damageTick2 = h3 + remainder;
double contribution = p1 * inverseCount * (damageTick1 >= hpBefore
? 1.0
: (damageTick2 >= (hpBefore - damageTick1 + healBetween) ? 1.0 : 0.0));
ko += contribution;
}

// Case E2: second swing connects (phase 1 does no damage)
{
int damageTick1 = roll;
int damageTick2 = roll;
double contribution = p2 * inverseCount * (damageTick1 >= hpBefore
? 1.0
: (damageTick2 >= (hpBefore - damageTick1 + healBetween) ? 1.0 : 0.0));
ko += contribution;
}

// Case E3: third swing connects (all damage tick k+1)
{
int damageTick1 = 0;
int damageTick2 = threeQuarterCeil + threeQuarterFloor;
double contribution = p3 * inverseCount * (damageTick1 >= hpBefore
? 1.0
: (damageTick2 >= (hpBefore - damageTick1 + healBetween) ? 1.0 : 0.0));
ko += contribution;
}

// Case E4: fourth swing connects (all damage tick k+1)
{
int damageTick1 = 0;
int damageTick2 = threeQuarterCeil + threeQuarterFloor;
double contribution = p4 * inverseCount * (damageTick1 >= hpBefore
? 1.0
: (damageTick2 >= (hpBefore - damageTick1 + healBetween) ? 1.0 : 0.0));
ko += contribution;
}
}

if (ko < 0.0)
{
ko = 0.0;
}
else if (ko > 1.0)
{
ko = 1.0;
}
return ko;
}

public static int getSpriteForSkill(Skill skill)
{
switch (skill)
Expand Down