Merge pull request #970 from sdburns1998/statusorbs

statusorbsplugin: Merge regenmeter and runenergy plugins
This commit is contained in:
Tyler Bochard
2019-07-11 00:39:27 -04:00
committed by GitHub
13 changed files with 916 additions and 833 deletions

View File

@@ -1,126 +0,0 @@
/*
* Copyright (c) 2018 Abex
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.regenmeter;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.geom.Arc2D;
import javax.inject.Inject;
import javax.inject.Singleton;
import net.runelite.api.Client;
import net.runelite.api.VarPlayer;
import net.runelite.api.widgets.Widget;
import net.runelite.api.widgets.WidgetInfo;
import net.runelite.client.ui.overlay.Overlay;
import net.runelite.client.ui.overlay.OverlayLayer;
import net.runelite.client.ui.overlay.OverlayPosition;
@Singleton
public class RegenMeterOverlay extends Overlay
{
private static final Color HITPOINTS_COLOR = brighter(0x9B0703);
private static final Color SPECIAL_COLOR = brighter(0x1E95B0);
private static final Color OVERLAY_COLOR = new Color(255, 255, 255, 60);
private static final double DIAMETER = 26D;
private static final int OFFSET = 27;
private static final Stroke STROKE = new BasicStroke(2f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
private final Client client;
private final RegenMeterPlugin plugin;
private Rectangle getBounds(WidgetInfo widgetInfo)
{
Widget widget = client.getWidget(widgetInfo);
if (widget == null || widget.isHidden())
{
return null;
}
return widget.getBounds();
}
private static Color brighter(int color)
{
float[] hsv = new float[3];
Color.RGBtoHSB(color >>> 16, (color >> 8) & 0xFF, color & 0xFF, hsv);
return Color.getHSBColor(hsv[0], 1.f, 1.f);
}
@Inject
public RegenMeterOverlay(final Client client, final RegenMeterPlugin plugin)
{
setPosition(OverlayPosition.DYNAMIC);
setLayer(OverlayLayer.ABOVE_WIDGETS);
this.client = client;
this.plugin = plugin;
}
@Override
public Dimension render(Graphics2D g)
{
g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
if (plugin.isShowHitpoints())
{
renderRegen(g, WidgetInfo.MINIMAP_HEALTH_ORB, plugin.getHitpointsPercentage(), HITPOINTS_COLOR);
}
if (plugin.isShowSpecial())
{
if (client.getVar(VarPlayer.SPECIAL_ATTACK_ENABLED) == 1)
{
final Rectangle bounds = getBounds(WidgetInfo.MINIMAP_SPEC_ORB);
if (bounds != null)
{
g.setColor(RegenMeterOverlay.OVERLAY_COLOR);
g.fillOval(
bounds.x + OFFSET,
bounds.y + (int) (bounds.height / 2D - (DIAMETER) / 2D),
(int) DIAMETER, (int) DIAMETER);
}
}
renderRegen(g, WidgetInfo.MINIMAP_SPEC_ORB, plugin.getSpecialPercentage(), SPECIAL_COLOR);
}
return null;
}
private void renderRegen(Graphics2D g, WidgetInfo widgetInfo, double percent, Color color)
{
final Rectangle bounds = getBounds(widgetInfo);
if (bounds != null)
{
Arc2D.Double arc = new Arc2D.Double(bounds.x + OFFSET, bounds.y + (bounds.height / 2 - DIAMETER / 2), DIAMETER, DIAMETER, 90.d, -360.d * percent, Arc2D.OPEN);
g.setStroke(STROKE);
g.setColor(color);
g.draw(arc);
}
}
}

View File

@@ -1,201 +0,0 @@
/*
* Copyright (c) 2019, Sean Dewar <https://github.com/seandewar>
* Copyright (c) 2018, Abex
* Copyright (c) 2018, Zimaya <https://github.com/Zimaya>
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.regenmeter;
import com.google.inject.Provides;
import javax.inject.Inject;
import javax.inject.Singleton;
import lombok.AccessLevel;
import lombok.Getter;
import net.runelite.api.Client;
import net.runelite.api.Constants;
import net.runelite.api.GameState;
import net.runelite.api.Prayer;
import net.runelite.api.Skill;
import net.runelite.api.VarPlayer;
import net.runelite.api.events.ConfigChanged;
import net.runelite.api.events.GameStateChanged;
import net.runelite.api.events.GameTick;
import net.runelite.api.events.VarbitChanged;
import net.runelite.client.Notifier;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.ui.overlay.OverlayManager;
@PluginDescriptor(
name = "Regeneration Meter",
description = "Track and show the hitpoints and special attack regeneration timers",
tags = {"combat", "health", "hitpoints", "special", "attack", "overlay", "notifications"}
)
@Singleton
public class RegenMeterPlugin extends Plugin
{
private static final int SPEC_REGEN_TICKS = 50;
private static final int NORMAL_HP_REGEN_TICKS = 100;
@Inject
private Client client;
@Inject
private OverlayManager overlayManager;
@Inject
private Notifier notifier;
@Inject
private RegenMeterOverlay overlay;
@Inject
private RegenMeterConfig config;
@Getter(AccessLevel.PACKAGE)
private double hitpointsPercentage;
@Getter(AccessLevel.PACKAGE)
private double specialPercentage;
private int ticksSinceSpecRegen;
private int ticksSinceHPRegen;
private boolean wasRapidHeal;
@Getter(AccessLevel.PACKAGE)
private boolean showHitpoints;
@Getter(AccessLevel.PACKAGE)
private boolean showSpecial;
private boolean showWhenNoChange;
private int getNotifyBeforeHpRegenSeconds;
@Provides
RegenMeterConfig provideConfig(ConfigManager configManager)
{
return configManager.getConfig(RegenMeterConfig.class);
}
@Override
protected void startUp() throws Exception
{
updateConfig();
overlayManager.add(overlay);
}
@Override
protected void shutDown() throws Exception
{
overlayManager.remove(overlay);
}
@Subscribe
private void onGameStateChanged(GameStateChanged ev)
{
if (ev.getGameState() == GameState.HOPPING || ev.getGameState() == GameState.LOGIN_SCREEN)
{
ticksSinceHPRegen = -2; // For some reason this makes this accurate
ticksSinceSpecRegen = 0;
}
}
@Subscribe
private void onVarbitChanged(VarbitChanged ev)
{
boolean isRapidHeal = client.isPrayerActive(Prayer.RAPID_HEAL);
if (wasRapidHeal != isRapidHeal)
{
ticksSinceHPRegen = 0;
}
wasRapidHeal = isRapidHeal;
}
@Subscribe
public void onGameTick(GameTick event)
{
if (client.getVar(VarPlayer.SPECIAL_ATTACK_PERCENT) == 1000)
{
// The recharge doesn't tick when at 100%
ticksSinceSpecRegen = 0;
}
else
{
ticksSinceSpecRegen = (ticksSinceSpecRegen + 1) % SPEC_REGEN_TICKS;
}
specialPercentage = ticksSinceSpecRegen / (double) SPEC_REGEN_TICKS;
int ticksPerHPRegen = NORMAL_HP_REGEN_TICKS;
if (client.isPrayerActive(Prayer.RAPID_HEAL))
{
ticksPerHPRegen /= 2;
}
ticksSinceHPRegen = (ticksSinceHPRegen + 1) % ticksPerHPRegen;
hitpointsPercentage = ticksSinceHPRegen / (double) ticksPerHPRegen;
int currentHP = client.getBoostedSkillLevel(Skill.HITPOINTS);
int maxHP = client.getRealSkillLevel(Skill.HITPOINTS);
if (currentHP == maxHP && !this.showWhenNoChange)
{
hitpointsPercentage = 0;
}
else if (currentHP > maxHP)
{
// Show it going down
hitpointsPercentage = 1 - hitpointsPercentage;
}
if (this.getNotifyBeforeHpRegenSeconds > 0 && currentHP < maxHP && shouldNotifyHpRegenThisTick(ticksPerHPRegen))
{
notifier.notify("[" + client.getLocalPlayer().getName() + "] regenerates their next hitpoint soon!");
}
}
private boolean shouldNotifyHpRegenThisTick(int ticksPerHPRegen)
{
// if the configured duration lies between two ticks, choose the earlier tick
final int ticksBeforeHPRegen = ticksPerHPRegen - ticksSinceHPRegen;
final int notifyTick = (int) Math.ceil(this.getNotifyBeforeHpRegenSeconds * 1000d / Constants.GAME_TICK_LENGTH);
return ticksBeforeHPRegen == notifyTick;
}
@Subscribe
public void onConfigChanged(ConfigChanged event)
{
if (event.getGroup().equals("regenmeter"))
{
updateConfig();
}
}
private void updateConfig()
{
this.showHitpoints = config.showHitpoints();
this.showSpecial = config.showSpecial();
this.showWhenNoChange = config.showWhenNoChange();
this.getNotifyBeforeHpRegenSeconds = config.getNotifyBeforeHpRegenSeconds();
}
}

View File

@@ -1,43 +0,0 @@
/*
* Copyright (c) 2018, Sean Dewar <https://github.com/seandewar>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.runenergy;
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
@ConfigGroup("runenergy")
public interface RunEnergyConfig extends Config
{
@ConfigItem(
keyName = "replaceOrbText",
name = "Replace orb text with run time left",
description = "Show the remaining run time (in seconds) next in the energy orb."
)
default boolean replaceOrbText()
{
return false;
}
}

View File

@@ -1,109 +0,0 @@
/*
* Copyright (c) 2018, Sean Dewar <https://github.com/seandewar>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.runenergy;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import javax.inject.Inject;
import javax.inject.Singleton;
import net.runelite.api.Client;
import net.runelite.api.Point;
import net.runelite.api.widgets.Widget;
import net.runelite.api.widgets.WidgetInfo;
import net.runelite.client.ui.overlay.Overlay;
import net.runelite.client.ui.overlay.OverlayLayer;
import net.runelite.client.ui.overlay.OverlayPosition;
import net.runelite.client.ui.overlay.tooltip.Tooltip;
import net.runelite.client.ui.overlay.tooltip.TooltipManager;
import org.apache.commons.lang3.StringUtils;
@Singleton
class RunEnergyOverlay extends Overlay
{
private final RunEnergyPlugin plugin;
private final Client client;
private final RunEnergyConfig config;
private final TooltipManager tooltipManager;
@Inject
private RunEnergyOverlay(final RunEnergyPlugin plugin, final Client client, final RunEnergyConfig config, final TooltipManager tooltipManager)
{
this.plugin = plugin;
this.client = client;
this.config = config;
this.tooltipManager = tooltipManager;
setPosition(OverlayPosition.DYNAMIC);
setLayer(OverlayLayer.ABOVE_WIDGETS);
}
@Override
public Dimension render(Graphics2D graphics)
{
final Widget runOrb = client.getWidget(WidgetInfo.MINIMAP_TOGGLE_RUN_ORB);
if (runOrb == null || runOrb.isHidden())
{
return null;
}
final Rectangle bounds = runOrb.getBounds();
if (bounds.getX() <= 0)
{
return null;
}
final Point mousePosition = client.getMouseCanvasPosition();
if (bounds.contains(mousePosition.getX(), mousePosition.getY()))
{
StringBuilder sb = new StringBuilder();
sb.append("Weight: ").append(client.getWeight()).append(" kg</br>");
if (config.replaceOrbText())
{
sb.append("Run Energy: ").append(client.getEnergy()).append("%");
}
else
{
sb.append("Run Time Remaining: ").append(plugin.getEstimatedRunTimeRemaining(false));
}
int secondsUntil100 = plugin.getEstimatedRecoverTimeRemaining();
if (secondsUntil100 > 0)
{
final int minutes = (int) Math.floor(secondsUntil100 / 60.0);
final int seconds = (int) Math.floor(secondsUntil100 - (minutes * 60.0));
sb.append("</br>").append("100% Energy In: ").append(minutes).append(':').append(StringUtils.leftPad(Integer.toString(seconds), 2, "0"));
}
tooltipManager.add(new Tooltip(sb.toString()));
}
return null;
}
}

View File

@@ -1,340 +0,0 @@
/*
* Copyright (c) 2018, Sean Dewar <https://github.com/seandewar>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.runenergy;
import com.google.common.collect.ImmutableSet;
import com.google.inject.Provides;
import javax.inject.Inject;
import javax.inject.Singleton;
import net.runelite.api.Client;
import net.runelite.api.Constants;
import net.runelite.api.EquipmentInventorySlot;
import net.runelite.api.InventoryID;
import net.runelite.api.Item;
import net.runelite.api.ItemContainer;
import static net.runelite.api.ItemID.AGILITY_CAPE;
import static net.runelite.api.ItemID.AGILITY_CAPET;
import static net.runelite.api.ItemID.GRACEFUL_BOOTS_11861;
import static net.runelite.api.ItemID.GRACEFUL_BOOTS_13589;
import static net.runelite.api.ItemID.GRACEFUL_BOOTS_13590;
import static net.runelite.api.ItemID.GRACEFUL_BOOTS_13601;
import static net.runelite.api.ItemID.GRACEFUL_BOOTS_13602;
import static net.runelite.api.ItemID.GRACEFUL_BOOTS_13613;
import static net.runelite.api.ItemID.GRACEFUL_BOOTS_13614;
import static net.runelite.api.ItemID.GRACEFUL_BOOTS_13625;
import static net.runelite.api.ItemID.GRACEFUL_BOOTS_13626;
import static net.runelite.api.ItemID.GRACEFUL_BOOTS_13637;
import static net.runelite.api.ItemID.GRACEFUL_BOOTS_13638;
import static net.runelite.api.ItemID.GRACEFUL_BOOTS_13677;
import static net.runelite.api.ItemID.GRACEFUL_BOOTS_13678;
import static net.runelite.api.ItemID.GRACEFUL_BOOTS_21076;
import static net.runelite.api.ItemID.GRACEFUL_BOOTS_21078;
import static net.runelite.api.ItemID.GRACEFUL_CAPE_11853;
import static net.runelite.api.ItemID.GRACEFUL_CAPE_13581;
import static net.runelite.api.ItemID.GRACEFUL_CAPE_13582;
import static net.runelite.api.ItemID.GRACEFUL_CAPE_13593;
import static net.runelite.api.ItemID.GRACEFUL_CAPE_13594;
import static net.runelite.api.ItemID.GRACEFUL_CAPE_13605;
import static net.runelite.api.ItemID.GRACEFUL_CAPE_13606;
import static net.runelite.api.ItemID.GRACEFUL_CAPE_13617;
import static net.runelite.api.ItemID.GRACEFUL_CAPE_13618;
import static net.runelite.api.ItemID.GRACEFUL_CAPE_13629;
import static net.runelite.api.ItemID.GRACEFUL_CAPE_13630;
import static net.runelite.api.ItemID.GRACEFUL_CAPE_13669;
import static net.runelite.api.ItemID.GRACEFUL_CAPE_13670;
import static net.runelite.api.ItemID.GRACEFUL_CAPE_21064;
import static net.runelite.api.ItemID.GRACEFUL_CAPE_21066;
import static net.runelite.api.ItemID.GRACEFUL_GLOVES_11859;
import static net.runelite.api.ItemID.GRACEFUL_GLOVES_13587;
import static net.runelite.api.ItemID.GRACEFUL_GLOVES_13588;
import static net.runelite.api.ItemID.GRACEFUL_GLOVES_13599;
import static net.runelite.api.ItemID.GRACEFUL_GLOVES_13600;
import static net.runelite.api.ItemID.GRACEFUL_GLOVES_13611;
import static net.runelite.api.ItemID.GRACEFUL_GLOVES_13612;
import static net.runelite.api.ItemID.GRACEFUL_GLOVES_13623;
import static net.runelite.api.ItemID.GRACEFUL_GLOVES_13624;
import static net.runelite.api.ItemID.GRACEFUL_GLOVES_13635;
import static net.runelite.api.ItemID.GRACEFUL_GLOVES_13636;
import static net.runelite.api.ItemID.GRACEFUL_GLOVES_13675;
import static net.runelite.api.ItemID.GRACEFUL_GLOVES_13676;
import static net.runelite.api.ItemID.GRACEFUL_GLOVES_21073;
import static net.runelite.api.ItemID.GRACEFUL_GLOVES_21075;
import static net.runelite.api.ItemID.GRACEFUL_HOOD_11851;
import static net.runelite.api.ItemID.GRACEFUL_HOOD_13579;
import static net.runelite.api.ItemID.GRACEFUL_HOOD_13580;
import static net.runelite.api.ItemID.GRACEFUL_HOOD_13591;
import static net.runelite.api.ItemID.GRACEFUL_HOOD_13592;
import static net.runelite.api.ItemID.GRACEFUL_HOOD_13603;
import static net.runelite.api.ItemID.GRACEFUL_HOOD_13604;
import static net.runelite.api.ItemID.GRACEFUL_HOOD_13615;
import static net.runelite.api.ItemID.GRACEFUL_HOOD_13616;
import static net.runelite.api.ItemID.GRACEFUL_HOOD_13627;
import static net.runelite.api.ItemID.GRACEFUL_HOOD_13628;
import static net.runelite.api.ItemID.GRACEFUL_HOOD_13667;
import static net.runelite.api.ItemID.GRACEFUL_HOOD_13668;
import static net.runelite.api.ItemID.GRACEFUL_HOOD_21061;
import static net.runelite.api.ItemID.GRACEFUL_HOOD_21063;
import static net.runelite.api.ItemID.GRACEFUL_LEGS_11857;
import static net.runelite.api.ItemID.GRACEFUL_LEGS_13585;
import static net.runelite.api.ItemID.GRACEFUL_LEGS_13586;
import static net.runelite.api.ItemID.GRACEFUL_LEGS_13597;
import static net.runelite.api.ItemID.GRACEFUL_LEGS_13598;
import static net.runelite.api.ItemID.GRACEFUL_LEGS_13609;
import static net.runelite.api.ItemID.GRACEFUL_LEGS_13610;
import static net.runelite.api.ItemID.GRACEFUL_LEGS_13621;
import static net.runelite.api.ItemID.GRACEFUL_LEGS_13622;
import static net.runelite.api.ItemID.GRACEFUL_LEGS_13633;
import static net.runelite.api.ItemID.GRACEFUL_LEGS_13634;
import static net.runelite.api.ItemID.GRACEFUL_LEGS_13673;
import static net.runelite.api.ItemID.GRACEFUL_LEGS_13674;
import static net.runelite.api.ItemID.GRACEFUL_LEGS_21070;
import static net.runelite.api.ItemID.GRACEFUL_LEGS_21072;
import static net.runelite.api.ItemID.GRACEFUL_TOP_11855;
import static net.runelite.api.ItemID.GRACEFUL_TOP_13583;
import static net.runelite.api.ItemID.GRACEFUL_TOP_13584;
import static net.runelite.api.ItemID.GRACEFUL_TOP_13595;
import static net.runelite.api.ItemID.GRACEFUL_TOP_13596;
import static net.runelite.api.ItemID.GRACEFUL_TOP_13607;
import static net.runelite.api.ItemID.GRACEFUL_TOP_13608;
import static net.runelite.api.ItemID.GRACEFUL_TOP_13619;
import static net.runelite.api.ItemID.GRACEFUL_TOP_13620;
import static net.runelite.api.ItemID.GRACEFUL_TOP_13631;
import static net.runelite.api.ItemID.GRACEFUL_TOP_13632;
import static net.runelite.api.ItemID.GRACEFUL_TOP_13671;
import static net.runelite.api.ItemID.GRACEFUL_TOP_13672;
import static net.runelite.api.ItemID.GRACEFUL_TOP_21067;
import static net.runelite.api.ItemID.GRACEFUL_TOP_21069;
import static net.runelite.api.ItemID.MAX_CAPE;
import net.runelite.api.Skill;
import net.runelite.api.Varbits;
import net.runelite.api.coords.WorldPoint;
import net.runelite.api.events.ConfigChanged;
import net.runelite.api.events.GameTick;
import net.runelite.api.widgets.Widget;
import net.runelite.api.widgets.WidgetInfo;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.ui.overlay.OverlayManager;
import org.apache.commons.lang3.StringUtils;
@PluginDescriptor(
name = "Run Energy",
description = "Show various information related to run energy",
tags = {"overlay", "stamina"}
)
@Singleton
public class RunEnergyPlugin extends Plugin
{
// TODO It would be nice if we have the IDs for just the equipped variants of the Graceful set items.
private static final ImmutableSet<Integer> ALL_GRACEFUL_HOODS = ImmutableSet.of(
GRACEFUL_HOOD_11851, GRACEFUL_HOOD_13579, GRACEFUL_HOOD_13580, GRACEFUL_HOOD_13591, GRACEFUL_HOOD_13592,
GRACEFUL_HOOD_13603, GRACEFUL_HOOD_13604, GRACEFUL_HOOD_13615, GRACEFUL_HOOD_13616, GRACEFUL_HOOD_13627,
GRACEFUL_HOOD_13628, GRACEFUL_HOOD_13667, GRACEFUL_HOOD_13668, GRACEFUL_HOOD_21061, GRACEFUL_HOOD_21063
);
private static final ImmutableSet<Integer> ALL_GRACEFUL_TOPS = ImmutableSet.of(
GRACEFUL_TOP_11855, GRACEFUL_TOP_13583, GRACEFUL_TOP_13584, GRACEFUL_TOP_13595, GRACEFUL_TOP_13596,
GRACEFUL_TOP_13607, GRACEFUL_TOP_13608, GRACEFUL_TOP_13619, GRACEFUL_TOP_13620, GRACEFUL_TOP_13631,
GRACEFUL_TOP_13632, GRACEFUL_TOP_13671, GRACEFUL_TOP_13672, GRACEFUL_TOP_21067, GRACEFUL_TOP_21069
);
private static final ImmutableSet<Integer> ALL_GRACEFUL_LEGS = ImmutableSet.of(
GRACEFUL_LEGS_11857, GRACEFUL_LEGS_13585, GRACEFUL_LEGS_13586, GRACEFUL_LEGS_13597, GRACEFUL_LEGS_13598,
GRACEFUL_LEGS_13609, GRACEFUL_LEGS_13610, GRACEFUL_LEGS_13621, GRACEFUL_LEGS_13622, GRACEFUL_LEGS_13633,
GRACEFUL_LEGS_13634, GRACEFUL_LEGS_13673, GRACEFUL_LEGS_13674, GRACEFUL_LEGS_21070, GRACEFUL_LEGS_21072
);
private static final ImmutableSet<Integer> ALL_GRACEFUL_GLOVES = ImmutableSet.of(
GRACEFUL_GLOVES_11859, GRACEFUL_GLOVES_13587, GRACEFUL_GLOVES_13588, GRACEFUL_GLOVES_13599, GRACEFUL_GLOVES_13600,
GRACEFUL_GLOVES_13611, GRACEFUL_GLOVES_13612, GRACEFUL_GLOVES_13623, GRACEFUL_GLOVES_13624, GRACEFUL_GLOVES_13635,
GRACEFUL_GLOVES_13636, GRACEFUL_GLOVES_13675, GRACEFUL_GLOVES_13676, GRACEFUL_GLOVES_21073, GRACEFUL_GLOVES_21075
);
private static final ImmutableSet<Integer> ALL_GRACEFUL_BOOTS = ImmutableSet.of(
GRACEFUL_BOOTS_11861, GRACEFUL_BOOTS_13589, GRACEFUL_BOOTS_13590, GRACEFUL_BOOTS_13601, GRACEFUL_BOOTS_13602,
GRACEFUL_BOOTS_13613, GRACEFUL_BOOTS_13614, GRACEFUL_BOOTS_13625, GRACEFUL_BOOTS_13626, GRACEFUL_BOOTS_13637,
GRACEFUL_BOOTS_13638, GRACEFUL_BOOTS_13677, GRACEFUL_BOOTS_13678, GRACEFUL_BOOTS_21076, GRACEFUL_BOOTS_21078
);
// Agility skill capes and the non-cosmetic Max capes also count for the Graceful set effect
private static final ImmutableSet<Integer> ALL_GRACEFUL_CAPES = ImmutableSet.of(
GRACEFUL_CAPE_11853, GRACEFUL_CAPE_13581, GRACEFUL_CAPE_13582, GRACEFUL_CAPE_13593, GRACEFUL_CAPE_13594,
GRACEFUL_CAPE_13605, GRACEFUL_CAPE_13606, GRACEFUL_CAPE_13617, GRACEFUL_CAPE_13618, GRACEFUL_CAPE_13629,
GRACEFUL_CAPE_13630, GRACEFUL_CAPE_13669, GRACEFUL_CAPE_13670, GRACEFUL_CAPE_21064, GRACEFUL_CAPE_21066,
AGILITY_CAPE, AGILITY_CAPET, MAX_CAPE
);
@Inject
private Client client;
@Inject
private OverlayManager overlayManager;
@Inject
private RunEnergyOverlay energyOverlay;
@Inject
private RunEnergyConfig energyConfig;
private boolean localPlayerRunningToDestination;
private WorldPoint prevLocalPlayerLocation;
@Provides
RunEnergyConfig getConfig(ConfigManager configManager)
{
return configManager.getConfig(RunEnergyConfig.class);
}
@Override
protected void startUp() throws Exception
{
overlayManager.add(energyOverlay);
}
@Override
protected void shutDown() throws Exception
{
overlayManager.remove(energyOverlay);
localPlayerRunningToDestination = false;
prevLocalPlayerLocation = null;
resetRunOrbText();
}
@Subscribe
public void onGameTick(GameTick event)
{
localPlayerRunningToDestination =
prevLocalPlayerLocation != null &&
client.getLocalDestinationLocation() != null &&
prevLocalPlayerLocation.distanceTo(client.getLocalPlayer().getWorldLocation()) > 1;
prevLocalPlayerLocation = client.getLocalPlayer().getWorldLocation();
if (energyConfig.replaceOrbText())
{
setRunOrbText(getEstimatedRunTimeRemaining(true));
}
}
@Subscribe
public void onConfigChanged(ConfigChanged event)
{
if (event.getGroup().equals("runenergy") && !energyConfig.replaceOrbText())
{
resetRunOrbText();
}
}
private void setRunOrbText(String text)
{
Widget runOrbText = client.getWidget(WidgetInfo.MINIMAP_RUN_ORB_TEXT);
if (runOrbText != null)
{
runOrbText.setText(text);
}
}
private void resetRunOrbText()
{
setRunOrbText(Integer.toString(client.getEnergy()));
}
String getEstimatedRunTimeRemaining(boolean inSeconds)
{
// Calculate the amount of energy lost every tick.
// Negative weight has the same depletion effect as 0 kg.
final int effectiveWeight = Math.max(client.getWeight(), 0);
double lossRate = (Math.min(effectiveWeight, 64) / 100.0) + 0.64;
if (client.getVar(Varbits.RUN_SLOWED_DEPLETION_ACTIVE) != 0)
{
lossRate *= 0.3; // Stamina effect reduces energy depletion to 30%
}
// Calculate the number of seconds left
final double secondsLeft = (client.getEnergy() * Constants.GAME_TICK_LENGTH) / (lossRate * 1000.0);
// Return the text
if (inSeconds)
{
return (int) Math.floor(secondsLeft) + "s";
}
else
{
final int minutes = (int) Math.floor(secondsLeft / 60.0);
final int seconds = (int) Math.floor(secondsLeft - (minutes * 60.0));
return minutes + ":" + StringUtils.leftPad(Integer.toString(seconds), 2, "0");
}
}
private boolean isLocalPlayerWearingFullGraceful()
{
final ItemContainer equipment = client.getItemContainer(InventoryID.EQUIPMENT);
if (equipment == null)
{
return false;
}
final Item[] items = equipment.getItems();
// Check that the local player is wearing enough items to be using full Graceful
// (the Graceful boots will have the highest slot index in the worn set).
if (items == null || items.length <= EquipmentInventorySlot.BOOTS.getSlotIdx())
{
return false;
}
return (ALL_GRACEFUL_HOODS.contains(items[EquipmentInventorySlot.HEAD.getSlotIdx()].getId()) &&
ALL_GRACEFUL_TOPS.contains(items[EquipmentInventorySlot.BODY.getSlotIdx()].getId()) &&
ALL_GRACEFUL_LEGS.contains(items[EquipmentInventorySlot.LEGS.getSlotIdx()].getId()) &&
ALL_GRACEFUL_GLOVES.contains(items[EquipmentInventorySlot.GLOVES.getSlotIdx()].getId()) &&
ALL_GRACEFUL_BOOTS.contains(items[EquipmentInventorySlot.BOOTS.getSlotIdx()].getId()) &&
ALL_GRACEFUL_CAPES.contains(items[EquipmentInventorySlot.CAPE.getSlotIdx()].getId()));
}
int getEstimatedRecoverTimeRemaining()
{
if (localPlayerRunningToDestination)
{
return -1;
}
// Calculate the amount of energy recovered every second
double recoverRate = (48 + client.getBoostedSkillLevel(Skill.AGILITY)) / 360.0;
if (isLocalPlayerWearingFullGraceful())
{
recoverRate *= 1.3; // 30% recover rate increase from Graceful set effect
}
// Calculate the number of seconds left
return (int) ((100 - client.getEnergy()) / recoverRate);
}
}

View File

@@ -1,5 +1,6 @@
/*
* Copyright (c) 2018 Abex
* Copyright (c) 2018, Sean Dewar <https://github.com/seandewar>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -22,29 +23,47 @@
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.regenmeter;
package net.runelite.client.plugins.statusorbs;
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
import net.runelite.client.config.Stub;
@ConfigGroup("regenmeter")
public interface RegenMeterConfig extends Config
@ConfigGroup("statusorbs")
public interface StatusOrbsConfig extends Config
{
@ConfigItem(
keyName = "showHitpoints",
name = "Show hitpoints regen",
description = "Show a ring around the hitpoints orb")
default boolean showHitpoints()
keyName = "hp",
name = "Hitpoints",
description = "",
position = 0
)
default Stub hp()
{
return new Stub();
}
@ConfigItem(
keyName = "dynamicHpHeart",
name = "Dynamic hitpoints heart",
description = "Changes the HP heart color to match players current affliction",
parent = "hp",
position = 1
)
default boolean dynamicHpHeart()
{
return true;
}
@ConfigItem(
keyName = "showSpecial",
name = "Show Spec. Attack regen",
description = "Show a ring around the Special Attack orb")
default boolean showSpecial()
keyName = "showHitpoints",
name = "Show hitpoints regen",
description = "Show a ring around the hitpoints orb",
parent = "hp",
position = 2
)
default boolean showHitpoints()
{
return true;
}
@@ -52,7 +71,10 @@ public interface RegenMeterConfig extends Config
@ConfigItem(
keyName = "showWhenNoChange",
name = "Show hitpoints regen at full hitpoints",
description = "Always show the hitpoints regen orb, even if there will be no stat change")
description = "Always show the hitpoints regen orb, even if there will be no stat change",
parent = "hp",
position = 3
)
default boolean showWhenNoChange()
{
return false;
@@ -61,10 +83,70 @@ public interface RegenMeterConfig extends Config
@ConfigItem(
keyName = "notifyBeforeHpRegenDuration",
name = "Hitpoint Regen Notification (seconds)",
description = "Notify approximately when your next hitpoint is about to regen. A value of 0 will disable notification."
description = "Notify approximately when your next hitpoint is about to regen. A value of 0 will disable notification.",
parent = "hp",
position = 4
)
default int getNotifyBeforeHpRegenSeconds()
{
return 0;
}
}
@ConfigItem(
keyName = "spec",
name = "Special attack",
description = "",
position = 5
)
default Stub spec()
{
return new Stub();
}
@ConfigItem(
keyName = "showSpecial",
name = "Show Spec. Attack regen",
description = "Show a ring around the Special Attack orb",
parent = "spec",
position = 6
)
default boolean showSpecial()
{
return true;
}
@ConfigItem(
keyName = "run",
name = "Run energy",
description = "",
position = 7
)
default Stub run()
{
return new Stub();
}
@ConfigItem(
keyName = "showRun",
name = "Show run energy regen",
description = "Show a ring around the run regen orb",
position = 8,
parent = "run"
)
default boolean showRun()
{
return true;
}
@ConfigItem(
keyName = "replaceOrbText",
name = "Replace run orb text with run time left",
description = "Show the remaining run time (in seconds) next in the energy orb",
position = 9,
parent = "run"
)
default boolean replaceOrbText()
{
return false;
}
}

View File

@@ -0,0 +1,228 @@
/*
* Copyright (c) 2018 Abex
* Copyright (c) 2018, Sean Dewar <https://github.com/seandewar>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.statusorbs;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.geom.Arc2D;
import javax.inject.Inject;
import net.runelite.api.Client;
import net.runelite.api.InventoryID;
import net.runelite.api.Point;
import net.runelite.api.Skill;
import net.runelite.api.VarPlayer;
import net.runelite.api.widgets.Widget;
import net.runelite.api.widgets.WidgetInfo;
import net.runelite.client.ui.overlay.Overlay;
import net.runelite.client.ui.overlay.OverlayLayer;
import net.runelite.client.ui.overlay.OverlayPosition;
import net.runelite.client.ui.overlay.tooltip.Tooltip;
import net.runelite.client.ui.overlay.tooltip.TooltipManager;
import net.runelite.client.util.Graceful;
import org.apache.commons.lang3.StringUtils;
public class StatusOrbsOverlay extends Overlay
{
private static final Color HITPOINTS_COLOR = brighter(0x9B0703);
private static final Color SPECIAL_COLOR = brighter(0x1E95B0);
private static final Color RUN_COLOR = new Color(255, 215, 0);
private static final Color OVERLAY_COLOR = new Color(255, 255, 255, 60);
private static final double DIAMETER = 26D;
private static final int OFFSET = 27;
private final Client client;
private final StatusOrbsPlugin plugin;
private final TooltipManager tooltipManager;
private long last = System.nanoTime();
private double percentHp;
private double lastHp;
private double percentSpec;
private double lastSpec;
private double percentRun;
private double lastRun;
private static Color brighter(int color)
{
float[] hsv = new float[3];
Color.RGBtoHSB(color >>> 16, (color >> 8) & 0xFF, color & 0xFF, hsv);
return Color.getHSBColor(hsv[0], 1.f, 1.f);
}
@Inject
public StatusOrbsOverlay(Client client, StatusOrbsPlugin plugin, TooltipManager tooltipManager)
{
setPosition(OverlayPosition.DYNAMIC);
setLayer(OverlayLayer.ABOVE_WIDGETS);
this.client = client;
this.plugin = plugin;
this.tooltipManager = tooltipManager;
}
@Override
public Dimension render(Graphics2D g)
{
g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
long current = System.nanoTime();
double ms = (current - last) / (double) 1000000;
if (plugin.isShowHitpoints())
{
if (lastHp == plugin.getHitpointsPercentage() && plugin.getHitpointsPercentage() != 0)
{
percentHp += ms * plugin.getHpPerMs();
}
else
{
percentHp = plugin.getHitpointsPercentage();
lastHp = plugin.getHitpointsPercentage();
}
renderRegen(g, WidgetInfo.MINIMAP_HEALTH_ORB, percentHp, HITPOINTS_COLOR);
}
if (plugin.isShowSpecial())
{
if (client.getVar(VarPlayer.SPECIAL_ATTACK_ENABLED) == 1)
{
final Widget widget = client.getWidget(WidgetInfo.MINIMAP_SPEC_ORB);
if (widget != null && !widget.isHidden())
{
final Rectangle bounds = widget.getBounds();
g.setColor(OVERLAY_COLOR);
g.fillOval(
bounds.x + OFFSET,
bounds.y + (int) (bounds.height / 2 - (DIAMETER) / 2),
(int) DIAMETER, (int) DIAMETER);
}
}
if (lastSpec == plugin.getSpecialPercentage() && plugin.getSpecialPercentage() != 0)
{
percentSpec += ms * plugin.getSpecPerMs();
}
else
{
percentSpec = plugin.getSpecialPercentage();
lastSpec = plugin.getSpecialPercentage();
}
renderRegen(g, WidgetInfo.MINIMAP_SPEC_ORB, percentSpec, SPECIAL_COLOR);
}
if (plugin.isReplaceOrbText())
{
final Widget runOrb = client.getWidget(WidgetInfo.MINIMAP_TOGGLE_RUN_ORB);
if (runOrb == null || runOrb.isHidden())
{
return null;
}
final Rectangle bounds = runOrb.getBounds();
if (bounds.getX() <= 0)
{
return null;
}
final Point mousePosition = client.getMouseCanvasPosition();
if (bounds.contains(mousePosition.getX(), mousePosition.getY()))
{
StringBuilder sb = new StringBuilder();
sb.append("Weight: ").append(client.getWeight()).append(" kg</br>");
if (plugin.isReplaceOrbText())
{
sb.append("Run Energy: ").append(client.getEnergy()).append("%");
}
else
{
sb.append("Run Time Remaining: ").append(plugin.getEstimatedRunTimeRemaining(false));
}
int secondsUntil100 = plugin.getEstimatedRecoverTimeRemaining();
if (secondsUntil100 > 0)
{
final int minutes = (int) Math.floor(secondsUntil100 / 60.0);
final int seconds = (int) Math.floor(secondsUntil100 - (minutes * 60.0));
sb.append("</br>").append("100% Energy In: ").append(minutes).append(':').append(StringUtils.leftPad(Integer.toString(seconds), 2, "0"));
}
tooltipManager.add(new Tooltip(sb.toString()));
}
}
if (plugin.isShowRun())
{
if (lastRun == plugin.getRunPercentage() && plugin.getRunPercentage() != 0)
{
double recoverRate = (48 + client.getBoostedSkillLevel(Skill.AGILITY)) / 360000.0;
if (Graceful.hasFullSet(client.getItemContainer(InventoryID.EQUIPMENT)))
{
recoverRate *= 1.3; // 30% recover rate increase from Graceful set effect
}
percentRun += ms * recoverRate;
}
else
{
percentRun = plugin.getRunPercentage();
lastRun = plugin.getRunPercentage();
}
renderRegen(g, WidgetInfo.MINIMAP_RUN_ORB, percentRun, RUN_COLOR);
}
last = current;
return null;
}
private void renderRegen(Graphics2D g, WidgetInfo widgetInfo, double percent, Color color)
{
Widget widget = client.getWidget(widgetInfo);
if (widget == null || widget.isHidden())
{
return;
}
Rectangle bounds = widget.getBounds();
Arc2D.Double arc = new Arc2D.Double(bounds.x + OFFSET, bounds.y + (bounds.height / 2 - DIAMETER / 2), DIAMETER, DIAMETER, 90.d, -360.d * percent, Arc2D.OPEN);
final Stroke STROKE = new BasicStroke(2f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
g.setStroke(STROKE);
g.setColor(color);
g.draw(arc);
}
}

View File

@@ -0,0 +1,490 @@
/*
* Copyright (c) 2019, Owain van Brakel <https://github.com/Owain94>
* Copyright (c) 2018, TheStonedTurtle <https://github.com/TheStonedTurtle>
* Copyright (c) 2018 Abex
* Copyright (c) 2018, Zimaya <https://github.com/Zimaya>
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.statusorbs;
import com.google.inject.Provides;
import java.awt.image.BufferedImage;
import javax.inject.Inject;
import lombok.AccessLevel;
import lombok.Getter;
import net.runelite.api.Client;
import net.runelite.api.Constants;
import net.runelite.api.GameState;
import net.runelite.api.InventoryID;
import net.runelite.api.Prayer;
import net.runelite.api.Skill;
import net.runelite.api.SpriteID;
import net.runelite.api.VarPlayer;
import net.runelite.api.Varbits;
import net.runelite.api.coords.WorldPoint;
import net.runelite.api.events.ConfigChanged;
import net.runelite.api.events.GameStateChanged;
import net.runelite.api.events.GameTick;
import net.runelite.api.events.VarbitChanged;
import net.runelite.api.widgets.Widget;
import net.runelite.api.widgets.WidgetInfo;
import net.runelite.client.Notifier;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.ui.overlay.OverlayManager;
import net.runelite.client.util.Graceful;
import net.runelite.client.util.ImageUtil;
import org.apache.commons.lang3.StringUtils;
@PluginDescriptor(
name = "Status Orbs",
description = "Configure settings for the Minimap orbs",
tags = {"minimap", "orb", "regen", "energy", "special"}
)
public class StatusOrbsPlugin extends Plugin
{
private static final BufferedImage HEART_DISEASE;
private static final BufferedImage HEART_POISON;
private static final BufferedImage HEART_VENOM;
static
{
HEART_DISEASE = ImageUtil.resizeCanvas(ImageUtil.getResourceStreamFromClass(StatusOrbsPlugin.class, "1067-DISEASE.png"), 26, 26);
HEART_POISON = ImageUtil.resizeCanvas(ImageUtil.getResourceStreamFromClass(StatusOrbsPlugin.class, "1067-POISON.png"), 26, 26);
HEART_VENOM = ImageUtil.resizeCanvas(ImageUtil.getResourceStreamFromClass(StatusOrbsPlugin.class, "1067-VENOM.png"), 26, 26);
}
private static final int SPEC_REGEN_TICKS = 50;
private static final int NORMAL_HP_REGEN_TICKS = 100;
@Inject
private Client client;
@Inject
private ClientThread clientThread;
@Inject
private ConfigManager configManager;
@Inject
private StatusOrbsConfig config;
@Inject
private StatusOrbsOverlay overlay;
@Inject
private OverlayManager overlayManager;
@Inject
private Notifier notifier;
@Getter
private double hitpointsPercentage;
@Getter
private double specialPercentage;
@Getter
private double runPercentage;
@Getter
private double hpPerMs;
@Getter
private double specPerMs = (double) 1 / (SPEC_REGEN_TICKS * 600);
// RegenMeter
private int ticksSinceSpecRegen;
private int ticksSinceHPRegen;
private boolean wasRapidHeal;
private double ticksSinceRunRegen;
// Run Energy
private int lastEnergy = 0;
private boolean localPlayerRunningToDestination;
private WorldPoint currPoint;
private WorldPoint prevLocalPlayerLocation;
private BufferedImage heart;
private boolean dynamicHpHeart;
@Getter(AccessLevel.PACKAGE)
private boolean showHitpoints;
private boolean showWhenNoChange;
private int getNotifyBeforeHpRegenSeconds;
@Getter(AccessLevel.PACKAGE)
private boolean showSpecial;
@Getter(AccessLevel.PACKAGE)
private boolean showRun;
@Getter(AccessLevel.PACKAGE)
private boolean replaceOrbText;
@Provides
StatusOrbsConfig provideConfig(ConfigManager configManager)
{
return configManager.getConfig(StatusOrbsConfig.class);
}
@Override
protected void startUp() throws Exception
{
migrateConfigs();
updateConfig();
overlayManager.add(overlay);
if (this.dynamicHpHeart && client.getGameState().equals(GameState.LOGGED_IN))
{
clientThread.invoke(this::checkHealthIcon);
}
}
@Override
protected void shutDown() throws Exception
{
overlayManager.remove(overlay);
localPlayerRunningToDestination = false;
prevLocalPlayerLocation = null;
resetRunOrbText();
if (this.dynamicHpHeart)
{
clientThread.invoke(this::resetHealthIcon);
}
}
@Subscribe
public void onConfigChanged(ConfigChanged event)
{
if (event.getGroup().equals("statusorbs"))
{
updateConfig();
switch (event.getKey())
{
case "replaceOrbText":
if (!this.replaceOrbText)
{
resetRunOrbText();
}
break;
case "dynamicHpHeart":
if (this.dynamicHpHeart)
{
checkHealthIcon();
}
else
{
resetHealthIcon();
}
break;
}
}
}
@Subscribe
private void onVarbitChanged(VarbitChanged e)
{
if (this.dynamicHpHeart)
{
checkHealthIcon();
}
boolean isRapidHeal = client.isPrayerActive(Prayer.RAPID_HEAL);
if (wasRapidHeal != isRapidHeal)
{
ticksSinceHPRegen = 0;
}
wasRapidHeal = isRapidHeal;
}
@Subscribe
private void onGameStateChanged(GameStateChanged ev)
{
if (ev.getGameState() == GameState.HOPPING || ev.getGameState() == GameState.LOGIN_SCREEN)
{
ticksSinceHPRegen = -2; // For some reason this makes this accurate
ticksSinceSpecRegen = 0;
ticksSinceRunRegen = -1;
}
}
@Subscribe
public void onGameTick(GameTick event)
{
if (client.getVar(VarPlayer.SPECIAL_ATTACK_PERCENT) == 1000)
{
// The recharge doesn't tick when at 100%
ticksSinceSpecRegen = 0;
}
else
{
ticksSinceSpecRegen = (ticksSinceSpecRegen + 1) % SPEC_REGEN_TICKS;
}
specialPercentage = ticksSinceSpecRegen / (double) SPEC_REGEN_TICKS;
int ticksPerHPRegen = NORMAL_HP_REGEN_TICKS;
hpPerMs = ticksPerHPRegen / (double) 6000000;
if (client.isPrayerActive(Prayer.RAPID_HEAL))
{
ticksPerHPRegen /= 2;
hpPerMs *= 2;
}
ticksSinceHPRegen = (ticksSinceHPRegen + 1) % ticksPerHPRegen;
hitpointsPercentage = ticksSinceHPRegen / (double) ticksPerHPRegen;
int currentHP = client.getBoostedSkillLevel(Skill.HITPOINTS);
int maxHP = client.getRealSkillLevel(Skill.HITPOINTS);
if (currentHP == maxHP && !this.showWhenNoChange)
{
hitpointsPercentage = 0;
}
else if (currentHP > maxHP)
{
// Show it going down
hitpointsPercentage = 1 - hitpointsPercentage;
}
// Run Energy
localPlayerRunningToDestination =
prevLocalPlayerLocation != null &&
client.getLocalDestinationLocation() != null &&
prevLocalPlayerLocation.distanceTo(client.getLocalPlayer().getWorldLocation()) > 1;
if (this.getNotifyBeforeHpRegenSeconds > 0 && currentHP < maxHP && shouldNotifyHpRegenThisTick(ticksPerHPRegen))
{
notifier.notify("[" + client.getLocalPlayer().getName() + "] regenerates their next hitpoint soon!");
}
localPlayerRunningToDestination =
prevLocalPlayerLocation != null &&
client.getLocalDestinationLocation() != null &&
prevLocalPlayerLocation.distanceTo(client.getLocalPlayer().getWorldLocation()) > 1;
prevLocalPlayerLocation = client.getLocalPlayer().getWorldLocation();
if (this.replaceOrbText)
{
setRunOrbText(getEstimatedRunTimeRemaining(true));
}
int currEnergy = client.getEnergy();
currPoint = client.getLocalPlayer().getWorldLocation();
if (currEnergy == 100 || (prevLocalPlayerLocation != null && currPoint.distanceTo(prevLocalPlayerLocation) > 1) || currEnergy < lastEnergy)
{
ticksSinceRunRegen = 0;
}
else if (currEnergy > lastEnergy)
{
if (runPercentage < 1)
{
ticksSinceRunRegen = (1 - runPercentage) / runRegenPerTick();
ticksSinceRunRegen = ticksSinceRunRegen > 1 ? 1 : ticksSinceRunRegen;
}
else
{
ticksSinceRunRegen = (runPercentage - 1) / runRegenPerTick();
}
}
else
{
ticksSinceRunRegen += 1;
}
runPercentage = ticksSinceRunRegen * runRegenPerTick();
prevLocalPlayerLocation = currPoint;
lastEnergy = currEnergy;
}
private boolean shouldNotifyHpRegenThisTick(int ticksPerHPRegen)
{
// if the configured duration lies between two ticks, choose the earlier tick
final int ticksBeforeHPRegen = ticksPerHPRegen - ticksSinceHPRegen;
final int notifyTick = (int) Math.ceil(this.getNotifyBeforeHpRegenSeconds * 1000d / Constants.GAME_TICK_LENGTH);
return ticksBeforeHPRegen == notifyTick;
}
private void setRunOrbText(String text)
{
Widget runOrbText = client.getWidget(WidgetInfo.MINIMAP_RUN_ORB_TEXT);
if (runOrbText != null)
{
runOrbText.setText(text);
}
}
private void resetRunOrbText()
{
setRunOrbText(Integer.toString(client.getEnergy()));
}
String getEstimatedRunTimeRemaining(boolean inSeconds)
{
// Calculate the amount of energy lost every 2 ticks (0.6 seconds).
// Negative weight has the same depletion effect as 0 kg.
final int effectiveWeight = Math.max(client.getWeight(), 0);
double lossRate = (Math.min(effectiveWeight, 64) / 100.0) + 0.64;
if (client.getVar(Varbits.RUN_SLOWED_DEPLETION_ACTIVE) != 0)
{
lossRate *= 0.3; // Stamina effect reduces energy depletion to 30%
}
// Calculate the number of seconds left
final double secondsLeft = (client.getEnergy() * 0.6) / lossRate;
// Return the text
if (inSeconds)
{
return (int) Math.floor(secondsLeft) + "s";
}
else
{
final int minutes = (int) Math.floor(secondsLeft / 60.0);
final int seconds = (int) Math.floor(secondsLeft - (minutes * 60.0));
return minutes + ":" + StringUtils.leftPad(Integer.toString(seconds), 2, "0");
}
}
int getEstimatedRecoverTimeRemaining()
{
if (localPlayerRunningToDestination)
{
return -1;
}
// Calculate the amount of energy recovered every second
double recoverRate = (48 + client.getBoostedSkillLevel(Skill.AGILITY)) / 360.0;
if (Graceful.hasFullSet(client.getItemContainer(InventoryID.EQUIPMENT)))
{
recoverRate *= 1.3; // 30% recover rate increase from Graceful set effect
}
// Calculate the number of seconds left
final double secondsLeft = (100 - client.getEnergy()) / recoverRate;
return (int) secondsLeft;
}
/**
* Check player afflictions to determine health icon
*/
private void checkHealthIcon()
{
BufferedImage newHeart;
int poison = client.getVar(VarPlayer.IS_POISONED);
if (poison >= 1000000)
{
newHeart = HEART_VENOM;
}
else if (poison > 0)
{
newHeart = HEART_POISON;
}
else if (client.getVar(VarPlayer.DISEASE_VALUE) > 0)
{
newHeart = HEART_DISEASE;
}
else
{
heart = null;
resetHealthIcon();
return;
}
// Only update sprites when the heart icon actually changes
if (newHeart != heart)
{
heart = newHeart;
client.getWidgetSpriteCache().reset();
client.getSpriteOverrides().put(SpriteID.MINIMAP_ORB_HITPOINTS_ICON, ImageUtil.getImageSprite(heart, client));
}
}
private double runRegenPerTick()
{
double recoverRate = (client.getBoostedSkillLevel(Skill.AGILITY) / 6d + 8) / 100;
if (Graceful.hasFullSet(client.getItemContainer(InventoryID.EQUIPMENT)))
{
return recoverRate * 1.3;
}
return recoverRate;
}
/**
* Ensure the HP Heart is the default Sprite
*/
private void resetHealthIcon()
{
client.getWidgetSpriteCache().reset();
client.getSpriteOverrides().remove(SpriteID.MINIMAP_ORB_HITPOINTS_ICON);
}
/**
* Migrates configs from runenergy and regenmeter to this plugin and deletes the old config values.
* This method should be removed after a reasonable amount of time.
*/
@Deprecated
private void migrateConfigs()
{
migrateConfig("regenmeter", "showHitpoints");
migrateConfig("regenmeter", "showSpecial");
migrateConfig("regenmeter", "showWhenNoChange");
migrateConfig("regenmeter", "notifyBeforeHpRegenDuration");
migrateConfig("runenergy", "replaceOrbText");
}
/**
* Wrapper for migrating individual config options
* This method should be removed after a reasonable amount of time.
*
* @param group old group name
* @param key key name to migrate
*/
@Deprecated
private void migrateConfig(String group, String key)
{
String value = configManager.getConfiguration(group, key);
if (value != null)
{
configManager.setConfiguration("statusorbs", key, value);
configManager.unsetConfiguration(group, key);
}
}
private void updateConfig()
{
this.dynamicHpHeart = config.dynamicHpHeart();
this.showHitpoints = config.showHitpoints();
this.showWhenNoChange = config.showWhenNoChange();
this.getNotifyBeforeHpRegenSeconds = config.getNotifyBeforeHpRegenSeconds();
this.showSpecial = config.showSpecial();
this.showRun = config.showRun();
this.replaceOrbText = config.replaceOrbText();
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright (c) 2018 raiyni <https://github.com/raiyni>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.util;
import com.google.common.collect.ImmutableSet;
import net.runelite.api.EquipmentInventorySlot;
import net.runelite.api.Item;
import net.runelite.api.ItemContainer;
import static net.runelite.api.ItemID.*;
public enum Graceful
{
// TODO: It would be nice if we have the IDs for just the equipped variants of the Graceful set items.
HOOD(
GRACEFUL_HOOD_11851, GRACEFUL_HOOD_13579, GRACEFUL_HOOD_13580, GRACEFUL_HOOD_13591, GRACEFUL_HOOD_13592,
GRACEFUL_HOOD_13603, GRACEFUL_HOOD_13604, GRACEFUL_HOOD_13615, GRACEFUL_HOOD_13616, GRACEFUL_HOOD_13627,
GRACEFUL_HOOD_13628, GRACEFUL_HOOD_13667, GRACEFUL_HOOD_13668, GRACEFUL_HOOD_21061, GRACEFUL_HOOD_21063
),
TOP(
GRACEFUL_TOP_11855, GRACEFUL_TOP_13583, GRACEFUL_TOP_13584, GRACEFUL_TOP_13595, GRACEFUL_TOP_13596,
GRACEFUL_TOP_13607, GRACEFUL_TOP_13608, GRACEFUL_TOP_13619, GRACEFUL_TOP_13620, GRACEFUL_TOP_13631,
GRACEFUL_TOP_13632, GRACEFUL_TOP_13671, GRACEFUL_TOP_13672, GRACEFUL_TOP_21067, GRACEFUL_TOP_21069
),
LEGS(
GRACEFUL_LEGS_11857, GRACEFUL_LEGS_13585, GRACEFUL_LEGS_13586, GRACEFUL_LEGS_13597, GRACEFUL_LEGS_13598,
GRACEFUL_LEGS_13609, GRACEFUL_LEGS_13610, GRACEFUL_LEGS_13621, GRACEFUL_LEGS_13622, GRACEFUL_LEGS_13633,
GRACEFUL_LEGS_13634, GRACEFUL_LEGS_13673, GRACEFUL_LEGS_13674, GRACEFUL_LEGS_21070, GRACEFUL_LEGS_21072
),
GLOVES(
GRACEFUL_GLOVES_11859, GRACEFUL_GLOVES_13587, GRACEFUL_GLOVES_13588, GRACEFUL_GLOVES_13599, GRACEFUL_GLOVES_13600,
GRACEFUL_GLOVES_13611, GRACEFUL_GLOVES_13612, GRACEFUL_GLOVES_13623, GRACEFUL_GLOVES_13624, GRACEFUL_GLOVES_13635,
GRACEFUL_GLOVES_13636, GRACEFUL_GLOVES_13675, GRACEFUL_GLOVES_13676, GRACEFUL_GLOVES_21073, GRACEFUL_GLOVES_21075
),
BOOTS(
GRACEFUL_BOOTS_11861, GRACEFUL_BOOTS_13589, GRACEFUL_BOOTS_13590, GRACEFUL_BOOTS_13601, GRACEFUL_BOOTS_13602,
GRACEFUL_BOOTS_13613, GRACEFUL_BOOTS_13614, GRACEFUL_BOOTS_13625, GRACEFUL_BOOTS_13626, GRACEFUL_BOOTS_13637,
GRACEFUL_BOOTS_13638, GRACEFUL_BOOTS_13677, GRACEFUL_BOOTS_13678, GRACEFUL_BOOTS_21076, GRACEFUL_BOOTS_21078
),
// Agility skill capes and the non-cosmetic Max capes also count for the Graceful set effect
CAPE(
GRACEFUL_CAPE_11853, GRACEFUL_CAPE_13581, GRACEFUL_CAPE_13582, GRACEFUL_CAPE_13593, GRACEFUL_CAPE_13594,
GRACEFUL_CAPE_13605, GRACEFUL_CAPE_13606, GRACEFUL_CAPE_13617, GRACEFUL_CAPE_13618, GRACEFUL_CAPE_13629,
GRACEFUL_CAPE_13630, GRACEFUL_CAPE_13669, GRACEFUL_CAPE_13670, GRACEFUL_CAPE_21064, GRACEFUL_CAPE_21066,
AGILITY_CAPE, AGILITY_CAPET, MAX_CAPE
);
private final ImmutableSet<Integer> ids;
Graceful(Integer... ids)
{
this.ids = ImmutableSet.copyOf(ids);
}
public static boolean hasFullSet(final ItemContainer equipment)
{
if (equipment == null)
{
return false;
}
final Item[] items = equipment.getItems();
if (equipment == null || items.length <= EquipmentInventorySlot.BOOTS.getSlotIdx())
{
return false;
}
return HOOD.ids.contains(items[EquipmentInventorySlot.HEAD.getSlotIdx()].getId())
&& TOP.ids.contains(items[EquipmentInventorySlot.BODY.getSlotIdx()].getId())
&& LEGS.ids.contains(items[EquipmentInventorySlot.LEGS.getSlotIdx()].getId())
&& GLOVES.ids.contains(items[EquipmentInventorySlot.GLOVES.getSlotIdx()].getId())
&& BOOTS.ids.contains(items[EquipmentInventorySlot.BOOTS.getSlotIdx()].getId())
&& CAPE.ids.contains(items[EquipmentInventorySlot.CAPE.getSlotIdx()].getId());
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB