Skip to content
Merged

Yama #140

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
4 changes: 2 additions & 2 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Please explain in reasonable detail what your series of changes does, and how it does it.
Make sure to link any relevant github issues which instigated your changes.

you can safely leave delete this comment as you fill out this text box.
you can safely leave or delete this comment as you fill out this text box.
Please leave the Summary and Testing sub titles.
-->

Expand All @@ -14,6 +14,6 @@ Please leave the Summary and Testing sub titles.
DO NOT leave this section empty, please add any relevant testing evidence (in game, unit tests, etc)
that show that your changes achieve the desired effect.

you can safely leave delete this comment as you fill out this text box.
you can safely leave or delete this comment as you fill out this text box.
Please leave the Summary and Testing sub titles.
-->
5 changes: 4 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,11 @@ java {
targetCompatibility = "1.11"
}

def props = new Properties()
file('runelite-plugin.properties').withInputStream { props.load(it) }

group = 'com.attacktimer'
version = '1.1'
version = props.getProperty('version')

tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
Expand Down
1 change: 1 addition & 0 deletions runelite-plugin.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
displayName=AttackTimer
author=ngraves95,Lexer747
build=standard
version=1.2.0
description=A plugin to countdown until your next attack
tags=pvm,timer,attack,combat,weapon
plugins=com.attacktimer.AttackTimerMetronomePlugin
2 changes: 1 addition & 1 deletion src/main/java/com/attacktimer/AnimationData.java
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ public enum AnimationData
MAGIC_ANCIENT_MULTI_TARGET_PVP(1979, AttackStyle.MAGIC, Spellbook.ANCIENT), // Burst & Barrage animations (tested all 8, different weapons)
MAGIC_ANCIENT_SINGLE_TARGET(10091, AttackStyle.MAGIC, Spellbook.ANCIENT), // Rush & Blitz animations (tested all 8, different weapons)
MAGIC_ANCIENT_SINGLE_TARGET_PVP(1978, AttackStyle.MAGIC, Spellbook.ANCIENT), // Rush & Blitz animations

MAGIC_ARCEUUS_DEMONBANE(8977, AttackStyle.MAGIC, Spellbook.ARCEUUS), // Also greater corruption, so that may accidentally trigger a manual-cast, but that's probably fine only affects Muspah
MAGIC_ARCEUUS_GRASP(8972, AttackStyle.MAGIC, Spellbook.ARCEUUS),

Expand Down
6 changes: 6 additions & 0 deletions src/main/java/com/attacktimer/AttackStyle.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,10 @@ public enum AttackStyle
this.name = name;
this.skills = skills;
}

@Override
public String toString()
{
return this.name;
}
}
139 changes: 121 additions & 18 deletions src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,17 @@
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

import com.attacktimer.ClientUtils.Utils;
import com.attacktimer.VariableSpeed.VariableSpeed;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.ByteArrayDataOutput;
import com.google.inject.Provides;
import java.awt.Color;
import java.awt.Dimension;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
Expand All @@ -44,19 +47,19 @@
import javax.inject.Inject;
import net.runelite.api.Actor;
import net.runelite.api.Client;
import net.runelite.api.EquipmentInventorySlot;
import net.runelite.api.InventoryID;
import net.runelite.api.Item;
import net.runelite.api.ItemContainer;
import net.runelite.api.NPC;
import net.runelite.api.Player;
import net.runelite.api.Skill;
import net.runelite.api.Varbits;
import net.runelite.api.events.ChatMessage;
import net.runelite.api.events.FakeXpDrop;
import net.runelite.api.events.GameTick;
import net.runelite.api.events.InteractingChanged;
import net.runelite.api.events.SoundEffectPlayed;
import net.runelite.api.events.StatChanged;
import net.runelite.api.events.VarClientIntChanged;
import net.runelite.api.events.VarbitChanged;
import net.runelite.api.gameval.VarPlayerID;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.events.ConfigChanged;
Expand Down Expand Up @@ -123,9 +126,18 @@ public enum AttackState
private int lastEquippingMonotonicValue = -1;
private int soundEffectTick = -1;
private int soundEffectId = -1;
private boolean isUsingMagic = false;

public int pendingEatDelayTicks = 0;

private ArrayDeque<Integer> specialPercentageEvents = new ArrayDeque<Integer>();
private Map<Skill, ArrayDeque<Integer>> combatExpEarned = Map.of(
Skill.MAGIC, new ArrayDeque<Integer>(),
Skill.RANGED, new ArrayDeque<Integer>(),
Skill.DEFENCE, new ArrayDeque<Integer>(),
Skill.STRENGTH, new ArrayDeque<Integer>(),
Skill.ATTACK, new ArrayDeque<Integer>()
);

private static final int UI_HIDE_DEBOUNCE_TICKS_MAX = 1;
private static final int ATTACK_DELAY_NONE = 0;
Expand Down Expand Up @@ -188,6 +200,10 @@ public void onVarbitChanged(VarbitChanged varbitChanged)
{
currentSpellBook = Spellbook.fromVarbit(varbitChanged.getValue());
}
if (varbitChanged.getVarpId() == VarPlayerID.SA_ENERGY)
{
specialPercentageEvents.addLast(varbitChanged.getValue());
}
}

// onVarbitChanged happens when the user causes some interaction therefore we can't rely on some fixed
Expand Down Expand Up @@ -227,6 +243,36 @@ public void onSoundEffectPlayed(SoundEffectPlayed event)
soundEffectId = event.getSoundId();
}

@Subscribe
protected void onFakeXpDrop(FakeXpDrop event)
{
if (!combatExpEarned.containsKey(event.getSkill()))
{
return;
}
combatExpEarned.get(event.getSkill()).addLast(event.getXp());
if (attackState == AttackState.DELAYED_FIRST_TICK)
{
// We recompute attack speed here incase the hitsplat mattered (e.g. purging staff)
performAttack();
}
}

@Subscribe
protected void onStatChanged(StatChanged event)
{
if (!combatExpEarned.containsKey(event.getSkill()))
{
return;
}
combatExpEarned.get(event.getSkill()).addLast(event.getXp());
if (attackState == AttackState.DELAYED_FIRST_TICK)
{
// We recompute attack speed here incase the hitsplat mattered (e.g. purging staff)
performAttack();
}
}

// endregion

@Provides
Expand All @@ -235,23 +281,41 @@ AttackTimerMetronomeConfig provideConfig(ConfigManager configManager)
return configManager.getConfig(AttackTimerMetronomeConfig.class);
}

private int getItemIdFromContainer(ItemContainer container, int slotID)
private int computeDamage(AttackStyle attackStyle, AttackProcedure atkType, AnimationData curAnimation)
{
if (container == null)
switch (atkType)
{
return -1;
case POWERED_STAVE:
// TODO not needed for any variable speed
return -1;
case MANUAL_AUTO_CAST:
if (attackStyle == AttackStyle.DEFENSIVE_CASTING || attackStyle == AttackStyle.DEFENSIVE)
{
// just use the defense exp to compute the damage
return Utils.getLastDelta(combatExpEarned.get(Skill.DEFENCE));
}
else
{
// deduct the fixed exp based on the spell
// (for now this only works for dark demon bane which awkwardly gives fractional exp)
var mageExp = Utils.getLastDelta(combatExpEarned.get(Skill.MAGIC));
if (curAnimation != AnimationData.MAGIC_ARCEUUS_DEMONBANE)
{
return -1;
}
return (int) Math.ceil(((double) mageExp - 43.5D) / 2.0D);
}
case MELEE_OR_RANGE:
// TODO not needed for any variable speed
return -1;
}
final Item item = container.getItem(slotID);
return (item != null) ? item.getId() : -1;
return -1;
}


private int getWeaponId()
{
int weaponId = getItemIdFromContainer(
client.getItemContainer(InventoryID.EQUIPMENT),
EquipmentInventorySlot.WEAPON.getSlotIdx()
);

final int weaponId = Utils.getWeaponId(client);
return WEAPON_ID_MAPPING_WORKAROUNDS.getOrDefault(weaponId, weaponId);
}

Expand Down Expand Up @@ -302,27 +366,35 @@ private int getMagicBaseSpeed(int weaponId)

private int getWeaponSpeed(int weaponId, PoweredStaves stave, AnimationData curAnimation, boolean matchesSpellbook)
{
var specDelta = Utils.getLastDelta(specialPercentageEvents);
int damageDealt = -1;
if (stave != null && stave.getAnimations().contains(curAnimation))
{
isUsingMagic = true;
damageDealt = computeDamage(Utils.getAttackStyle(client), AttackProcedure.POWERED_STAVE, curAnimation);
// We are currently dealing with a staves in which case we can make decisions based on the
// spellbook flag. We can only improve this by using a deprecated API to check the projectile
// matches the stave rather than a manual spell, but this is good enough for now.
return VariableSpeed.computeSpeed(client, curAnimation, AttackProcedure.POWERED_STAVE, 4);
return VariableSpeed.computeSpeed(client, curAnimation, AttackProcedure.POWERED_STAVE, damageDealt, specDelta, 4);
}

if (matchesSpellbook && isManualCasting(curAnimation))
{
isUsingMagic = true;
damageDealt = computeDamage(Utils.getAttackStyle(client), AttackProcedure.MANUAL_AUTO_CAST, curAnimation);
// You can cast with anything equipped in which case we shouldn't look to invent for speed.
return VariableSpeed.computeSpeed(client, curAnimation, AttackProcedure.MANUAL_AUTO_CAST, getMagicBaseSpeed(weaponId));
return VariableSpeed.computeSpeed(client, curAnimation, AttackProcedure.MANUAL_AUTO_CAST, damageDealt, specDelta,getMagicBaseSpeed(weaponId));
}

isUsingMagic = false;
damageDealt = computeDamage(Utils.getAttackStyle(client), AttackProcedure.MELEE_OR_RANGE, curAnimation);
ItemStats weaponStats = getWeaponStats(weaponId);
if (weaponStats == null)
{
return VariableSpeed.computeSpeed(client, curAnimation, AttackProcedure.MELEE_OR_RANGE, 4); // Assume barehanded == 4t
return VariableSpeed.computeSpeed(client, curAnimation, AttackProcedure.MELEE_OR_RANGE, damageDealt, specDelta, 4); // Assume barehanded == 4t
}
// Deadline for next available attack.
return VariableSpeed.computeSpeed(client, curAnimation, AttackProcedure.MELEE_OR_RANGE, weaponStats.getEquipment().getAspeed());
return VariableSpeed.computeSpeed(client, curAnimation, AttackProcedure.MELEE_OR_RANGE, damageDealt, specDelta, weaponStats.getEquipment().getAspeed());
}

private static final List<Integer> SPECIAL_NPCS = Arrays.asList(10507, 9435, 9438, 9441, 9444); // Combat Dummy + Nightmare Pillars
Expand Down Expand Up @@ -438,6 +510,7 @@ public void onChatMessage(ChatMessage event)
// We should always add eat delay
pendingEatDelayTicks += attackDelay;
}
VariableSpeed.onChatMessage(client, event);
}

// onInteractingChanged is the driver for detecting if the player attacked out side the usual tick window
Expand All @@ -456,6 +529,7 @@ public void onInteractingChanged(InteractingChanged interactingChanged)
switch (attackState)
{
case NOT_ATTACKING:
isUsingMagic = false;
// If not previously attacking, this action can result in a queued attack or
// an instant attack. If its queued, don't trigger the cooldown yet.
if (isPlayerAttacking())
Expand Down Expand Up @@ -523,6 +597,17 @@ public void onGameTick(GameTick tick)
// clamp the attackDelayHoldoffTicks at -20, this is so we correctly account for eats even when not
// attacking, but don't count down forever.
attackDelayHoldoffTicks = Math.max(-20, attackDelayHoldoffTicks - 1);
if (specialPercentageEvents.size() > 5)
{
specialPercentageEvents.removeFirst();
}
for (var q : combatExpEarned.values())
{
if (q.size() > 5)
{
q.removeFirst();
}
}
}


Expand Down Expand Up @@ -551,6 +636,7 @@ protected void shutDown() throws Exception
attackDelayHoldoffTicks = 0;
}

@VisibleForTesting
public void writeState(ByteArrayDataOutput outChannel)
{
StringBuilder sb = new StringBuilder();
Expand All @@ -570,4 +656,21 @@ public void writeState(ByteArrayDataOutput outChannel)
outChannel.write(bytes);
}
private static final String SEPARATOR = ", ";


public void onRender()
{
int delta = 0;
delta = VariableSpeed.SHADOW_CRASH.onRender(client, attackDelayHoldoffTicks, isUsingMagic);

if (delta != 0)
{
attackDelayHoldoffTicks += delta;
// if a change in attack delay would cause the delay to be less than 0 we hide the display
if (attackDelayHoldoffTicks < 0)
{
attackState = AttackState.NOT_ATTACKING;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public AttackTimerMetronomeTileOverlay(Client client, AttackTimerMetronomeConfig
@Override
public Dimension render(Graphics2D graphics)
{
plugin.onRender();
player = client.getLocalPlayer();
plugin.renderedState = plugin.attackState;
if (plugin.attackState == AttackTimerMetronomePlugin.AttackState.NOT_ATTACKING)
Expand Down
Loading
Loading