chore: init
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
.gradle
|
||||||
|
build
|
||||||
|
.idea/
|
||||||
|
.project
|
||||||
|
.settings/
|
||||||
|
.classpath
|
||||||
|
nbactions.xml
|
||||||
|
nb-configuration.xml
|
||||||
|
nbproject/
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# EssentialsPlus - Entwicklungsrichtlinien & Build-Workflow
|
||||||
|
|
||||||
|
## ⚠️ EXTREM WICHTIGER DEPLOYMENT- & RELEASE-WORKFLOW
|
||||||
|
|
||||||
|
Bei **JEDEM** Update / Build müssen immer folgende Schritte ausgeführt werden:
|
||||||
|
|
||||||
|
### 1. Lokales Bolt Launcher Deployment
|
||||||
|
- `./gradlew deploy` baut die Fat-Jar (`shadowJar`) und installiert sie atomar nach:
|
||||||
|
`~/.local/share/bolt-launcher/custom-runelite.jar`
|
||||||
|
- Zusätzlich wird die Jar immer nach `/home/hikyu/Downloads/RuneLite.jar` kopiert.
|
||||||
|
|
||||||
|
### 2. Git Release Upload (`Runelite-EssentialsPlus`)
|
||||||
|
- **Remote**: `ssh://git@git.gapspace.de:2222/Hikyu/Runelite-EssentialsPlus.git`
|
||||||
|
- **Lokaler Release-Ordner**: `/home/hikyu/Documents/projects/Runelite-EssentialsPlus`
|
||||||
|
- **Direkter Download-Link**: `https://gapspace.de/Hikyu/Runelite-EssentialsPlus/raw/branch/master/RuneLite.jar`
|
||||||
|
- **WICHTIGE REGELN**:
|
||||||
|
1. In diesem Repository darf **NUR** die einzelne Datei `RuneLite.jar` liegen (keine Ordner, keine Skripte, keine anderen Dateien).
|
||||||
|
2. Es darf **keine Versions-Historie** auf dem Server akkumulieren: Alte Versionen werden bei jedem Update überschrieben.
|
||||||
|
3. Der Befehl zum Aktualisieren des Release-Repos lautet:
|
||||||
|
```bash
|
||||||
|
cp build/libs/bank-highlighter-1.0-SNAPSHOT-all.jar /home/hikyu/Documents/projects/Runelite-EssentialsPlus/RuneLite.jar
|
||||||
|
cd /home/hikyu/Documents/projects/Runelite-EssentialsPlus
|
||||||
|
git add RuneLite.jar
|
||||||
|
git commit --amend -m "RuneLite EssentialsPlus (neuestes Update)" --date=now
|
||||||
|
git push -f origin master
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Plugin-Module Übersicht
|
||||||
|
- **Deterministischer Attack Timer** (`com.example.essentialsplus.helpers.combat.*`):
|
||||||
|
- 100% deterministische Berechnung von Waffengeschwindigkeit, Ranged Rapid-Modus (-1 Tick), Powered Staves (4 Ticks), Manual/Autocast Spells (5 Ticks, Harmonised 4 Ticks, Twinflame 6 Ticks) inkl. Casting-Sound-Erkennung.
|
||||||
|
- Spezielle Boss-Mechaniken: Tormented Demons, Blood Moon Set, Red Keris, Scurrius, Amoxliatl, ToA Warden Skulls, Eye of Ayak, Doom of Mokhaiotl, Royal Titans, Maggot King, Yama Shadow Crash.
|
||||||
|
- Essens-/Trankverzögerungen (Fast Foods 2 Ticks, Standard 3 Ticks, Vampyrium Tartare 4 Ticks).
|
||||||
|
- Design und Einstellungen in `AttackTimerOverlay.java` und `EssentialsPlusConfig.java` bleiben optisch immer unverändert.
|
||||||
|
- **Item Charges Helper** (`com.example.essentialsplus.helpers.ItemCharges*`):
|
||||||
|
- Lückenlose Ladungsanzeige & Persistenz für Barrows (Dharok etc. in %), Blowpipe (Darts & Scales getrennt/kombiniert), Coal Bag, Gem Bag, Herb Sack, Fish Barrel, Arclight/Emberlight, Ringe, Jewelry, Crystal Tools/Armor.
|
||||||
|
- **Wealth & Currency Store**: Vermögen, GE-Buy-Limits und Minigame-Währungen.
|
||||||
|
- **Loot & Combat Tracker**: Drop Tables, Sessions, DPS-Rechner.
|
||||||
|
- **Bank Highlighter**: Farbhervorhebung in Bank-Tabs.
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
# RuneLite Example Plugin ("Hello World")
|
||||||
|
|
||||||
|
Ein einsatzbereites RuneLite-Plugin-Beispielprojekt. Das Plugin sendet eine **"Hello World!"**-Nachricht in den Ingame-Chat, sobald du dich einloggst oder das Plugin aktivierst.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Projektstruktur
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── build.gradle # Gradle Build-Konfiguration
|
||||||
|
├── settings.gradle # Projektname
|
||||||
|
├── gradlew / gradlew.bat # Gradle Wrapper (keine manuelle Gradle-Installation nötig)
|
||||||
|
└── src
|
||||||
|
├── main/java/com/example
|
||||||
|
│ ├── ExamplePlugin.java # Hauptklasse des Plugins
|
||||||
|
│ └── ExampleConfig.java # Einstellungs-Menü im RuneLite Config-Panel
|
||||||
|
└── test/java/com/example
|
||||||
|
└── ExamplePluginTest.java # Startet RuneLite mit dem geladenen Plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Wie starte / teste ich das Plugin lokal?
|
||||||
|
|
||||||
|
Der Standard- und einfachste Weg in der RuneLite-Entwicklung ist der **integrierte Test-Client**. Dadurch musst du keine JAR-Dateien manuell kopieren oder den offiziellen Client patchen.
|
||||||
|
|
||||||
|
### Option 1: Über das Terminal / CLI (am schnellsten)
|
||||||
|
|
||||||
|
Führe einfach folgenden Befehl im Projektordner aus:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew run
|
||||||
|
```
|
||||||
|
|
||||||
|
*Unter Windows:*
|
||||||
|
```cmd
|
||||||
|
gradlew.bat run
|
||||||
|
```
|
||||||
|
|
||||||
|
RuneLite startet im Entwicklermodus mit deinem geladenen Plugin. Sobald du dich mit deinem Account einloggst, erscheint im Chat:
|
||||||
|
> `Example Plugin: Hello World!`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Option 2: Über eine IDE (IntelliJ IDEA / VS Code / Eclipse)
|
||||||
|
|
||||||
|
1. **IntelliJ IDEA öffnen** $\rightarrow$ `Open` $\rightarrow$ diesen Projektordner auswählen $\rightarrow$ als Gradle-Projekt importieren.
|
||||||
|
2. Zu [`ExamplePluginTest.java`](file:///home/hikyu/Documents/projects/orsrs-plugin-bank-highlighter/src/test/java/com/example/ExamplePluginTest.java) navigieren.
|
||||||
|
3. Den grünen **Play-Button ▶** neben `main()` anklicken (oder `Shift + F10`).
|
||||||
|
4. RuneLite startet sofort mit Hot-Reloading & Debugging-Möglichkeiten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Wie funktioniert das "Hello World"?
|
||||||
|
|
||||||
|
In [`ExamplePlugin.java`](file:///home/hikyu/Documents/projects/orsrs-plugin-bank-highlighter/src/main/java/com/example/ExamplePlugin.java):
|
||||||
|
1. **Beim Login:** Das Event `@Subscribe public void onGameStateChanged(GameStateChanged gameStateChanged)` fängt `GameState.LOGGED_IN` ab und sendet die Nachricht.
|
||||||
|
2. **Bei Plugin-Aktivierung:** In `startUp()` wird geprüft, ob du bereits eingeloggt bist (`client.getGameState() == GameState.LOGGED_IN`), und sendet ebenfalls die Nachricht.
|
||||||
|
3. **Anpassbar:** Die Nachricht kann im RuneLite Einstellungsmenü (unter **Example Plugin** in der Seitenleiste) konfiguriert werden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Standalone-JAR bauen (ShadowJar)
|
||||||
|
|
||||||
|
Falls du eine eigenständige JAR-Datei benötigst:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew shadowJar
|
||||||
|
```
|
||||||
|
|
||||||
|
Die fertige JAR befindet sich anschließend in:
|
||||||
|
`build/libs/bank-highlighter-1.0-SNAPSHOT-all.jar`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ Installation in Bolt (lokal)
|
||||||
|
|
||||||
|
Die Plugins (ExamplePlugin, LiveDashboardPlugin, EssentialsPlusPlugin) werden über
|
||||||
|
den Einstiegspunkt `ExamplePluginTest.main` direkt als Builtin-Externals in den
|
||||||
|
RuneLite-Client injiziert — es ist **kein** Bolt-Plugin-Manifest (RS3) nötig.
|
||||||
|
|
||||||
|
### 1. Einmalige Bolt-Konfiguration
|
||||||
|
|
||||||
|
In `~/.config/bolt-launcher/launcher.json` muss gesetzt sein:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"runelite_use_custom_jar": true,
|
||||||
|
"runelite_custom_jar": "/home/hikyu/.local/share/bolt-launcher/custom-runelite.jar",
|
||||||
|
"osrs_launch_command": "java -jar /home/hikyu/.local/share/bolt-launcher/custom-runelite.jar"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Bauen & installieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Befehl baut den Shadow-Jar und kopiert ihn **atomar** nach
|
||||||
|
`~/.local/share/bolt-launcher/custom-runelite.jar`, so dass er auch bei laufendem
|
||||||
|
Bolt/RuneLite sicher ersetzt werden kann (build.gradle `deploy`).
|
||||||
|
|
||||||
|
### 3. Automatische Neuinstallation bei jeder Änderung
|
||||||
|
|
||||||
|
Lasse den Continuous-Build im Projektordner laufen — er überwacht die Quelldateien
|
||||||
|
und installiert automatisch neu, sobald etwas geändert wird:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew deploy -t
|
||||||
|
```
|
||||||
|
|
||||||
|
*(Beenden mit `Ctrl+C`. Der Watcher kompiliert und kopiert die JAR bei jeder
|
||||||
|
Quellcode-Änderung selbstständig.)*
|
||||||
|
|
||||||
|
### 4. Start in Bolt
|
||||||
|
|
||||||
|
1. Bolt starten → **RuneLite** wählen → einloggen.
|
||||||
|
2. „Essentials Plus" erscheint mit einem Icon in der Seitenleiste.
|
||||||
|
3. Falls nicht sichtbar: RuneLite-Hamburger → **Configuration** → **External Plugins** → „Essentials Plus" aktivieren.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🪟 Windows / Jagex Launcher (offizielles RuneLite)
|
||||||
|
|
||||||
|
Auf Windows wird RuneLite über den **Jagex Launcher** gestartet. Dort lässt sich der
|
||||||
|
Bolt-Trick (eigenes Client-JAR mit `ExamplePluginTest.main`) **nicht** verwenden, weil
|
||||||
|
der Jagex Launcher immer die offizielle RuneLite-EXE startet. Stattdessen nutzt man das
|
||||||
|
offizielle **Side-Loading** von RuneLite: Beim Client-Start lädt RuneLite alle `.jar`
|
||||||
|
Dateien aus `~\.runelite\sideloaded-plugins\` nach – sofern der **Developer Mode**
|
||||||
|
aktiviert ist. `runelite-plugin.properties` wird dabei nicht gebraucht.
|
||||||
|
|
||||||
|
### 1. Plugin-JAR bauen (klein, ohne Client)
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
gradlew.bat jar
|
||||||
|
```
|
||||||
|
|
||||||
|
Ergebnis: `build\libs\bank-highlighter-1.0-SNAPSHOT.jar` (enthält nur `com.example.*`
|
||||||
|
Klassen, keine `net.runelite.*` – der richtige Jar-Typ zum Side-Loaden).
|
||||||
|
|
||||||
|
### 2. JAR in den Side-Load-Ordner kopieren
|
||||||
|
|
||||||
|
Ordner anlegen (falls nicht vorhanden) und das JAR hineinkopieren:
|
||||||
|
|
||||||
|
```
|
||||||
|
%USERPROFILE%\.runelite\sideloaded-plugins\bank-highlighter.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
*Beispiel via Explorer: `Start` → `%USERPROFILE%\.runelite\` eingeben → Ordner
|
||||||
|
`sideloaded-plugins` erstellen → JAR hineinlegen.*
|
||||||
|
|
||||||
|
### 3. Developer Mode in RuneLite aktivieren
|
||||||
|
|
||||||
|
1. RuneLite über den **Jagex Launcher** starten.
|
||||||
|
2. Schraubenschlüssel (Configuration) → ganz nach unten scrollen → **RuneLite**-Reiter.
|
||||||
|
3. **„Enable Developer Mode"** aktivieren (Hinweis bestätigen).
|
||||||
|
4. RuneLite vollständig schließen und erneut starten.
|
||||||
|
|
||||||
|
### 4. Plugin aktivieren
|
||||||
|
|
||||||
|
Nach dem Neustart erscheinen die Plugins in der Plugin-Liste (Plug-Icon) unter
|
||||||
|
**External Plugins** (unten).
|
||||||
|
|
||||||
|
- **Essentials Plus** aktivieren.
|
||||||
|
- Weitere: **Example Plugin**, **Live Dashboard**.
|
||||||
|
|
||||||
|
### Automatisch aktualisieren
|
||||||
|
|
||||||
|
Sobald sich etwas im Quellcode ändert, im Projektordner laufen lassen:
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
gradlew.bat jar -t
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Watcher kompiliert das schlanke JAR automatisch neu. Danach **RuneLite einmal
|
||||||
|
neu starten**, damit das neue JAR geladen wird (Side-Loaded-Plugins werden nur beim
|
||||||
|
Client-Start eingelesen – nicht live).
|
||||||
|
|
||||||
|
> ⚠️ **Hinweis:** Nach einem OSRS-Game-Update muss das JAR frisch gebaut werden
|
||||||
|
> (`gradlew.bat jar` bzw. der Watcher), damit die API (`net.runelite.api.gameval`, …)
|
||||||
|
> zum laufenden Client-Rev passt. Läuft ein älteres JAR, können Plugins mit Fehlern
|
||||||
|
> abstürzen.
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
title RuneLite EssentialsPlus Updater
|
||||||
|
color 0B
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ========================================================
|
||||||
|
echo RuneLite EssentialsPlus Updater
|
||||||
|
echo ========================================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
set "SCRIPT_DIR=%~dp0"
|
||||||
|
set "TARGET_JAR=%SCRIPT_DIR%RuneLite.jar"
|
||||||
|
set "OLD_JAR=%SCRIPT_DIR%RuneLite.jar.old"
|
||||||
|
set "TMP_JAR=%SCRIPT_DIR%RuneLite.jar.tmp"
|
||||||
|
set "DOWNLOAD_URL=https://gapspace.de/Hikyu/Runelite-EssentialsPlus/raw/branch/master/RuneLite.jar"
|
||||||
|
|
||||||
|
:: -----------------------------------------------------------
|
||||||
|
:: Schritt 1: Bestehende Version sichern
|
||||||
|
:: -----------------------------------------------------------
|
||||||
|
echo [1/3] Sichere bestehende Version...
|
||||||
|
if exist "%TARGET_JAR%" (
|
||||||
|
if exist "%OLD_JAR%" del /f /q "%OLD_JAR%" 2>nul
|
||||||
|
move /y "%TARGET_JAR%" "%OLD_JAR%" >nul
|
||||||
|
if !errorlevel! equ 0 (
|
||||||
|
echo - Alte Version gesichert als: RuneLite.jar.old
|
||||||
|
) else (
|
||||||
|
echo [!] FEHLER: Konnte RuneLite.jar nicht verschieben.
|
||||||
|
echo Bitte schliesse RuneLite / Bolt Launcher und versuche es erneut.
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
) else (
|
||||||
|
echo - Keine vorherige RuneLite.jar gefunden (Neuinstallation).
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: -----------------------------------------------------------
|
||||||
|
:: Schritt 2: Neueste Version herunterladen
|
||||||
|
:: -----------------------------------------------------------
|
||||||
|
echo [2/3] Lade neueste Version herunter...
|
||||||
|
echo Quelle: %DOWNLOAD_URL%
|
||||||
|
echo.
|
||||||
|
|
||||||
|
if exist "%TMP_JAR%" del /f /q "%TMP_JAR%" 2>nul
|
||||||
|
|
||||||
|
where curl >nul 2>nul
|
||||||
|
if !errorlevel! equ 0 (
|
||||||
|
curl -# -L -f -o "%TMP_JAR%" "%DOWNLOAD_URL%"
|
||||||
|
) else (
|
||||||
|
echo (Verwende PowerShell fuer Download...)
|
||||||
|
powershell -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%TMP_JAR%')"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not exist "%TMP_JAR%" (
|
||||||
|
echo.
|
||||||
|
echo [!] FEHLER: Download fehlgeschlagen!
|
||||||
|
if exist "%OLD_JAR%" (
|
||||||
|
echo Stelle alte Version wieder her...
|
||||||
|
move /y "%OLD_JAR%" "%TARGET_JAR%" >nul
|
||||||
|
)
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
:: Datei fertigstellen
|
||||||
|
move /y "%TMP_JAR%" "%TARGET_JAR%" >nul
|
||||||
|
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: -----------------------------------------------------------
|
||||||
|
:: Schritt 3: Fertigstellung
|
||||||
|
:: -----------------------------------------------------------
|
||||||
|
echo [3/3] Aktualisierung erfolgreich abgeschlossen!
|
||||||
|
echo - Datei: RuneLite.jar
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: Optional: Falls im Bolt Launcher Verzeichnis, auch custom-runelite.jar synchronisieren
|
||||||
|
if exist "%LOCALAPPDATA%\bolt-launcher" (
|
||||||
|
copy /y "%TARGET_JAR%" "%LOCALAPPDATA%\bolt-launcher\custom-runelite.jar" >nul 2>nul
|
||||||
|
)
|
||||||
|
if exist "%APPDATA%\bolt-launcher" (
|
||||||
|
copy /y "%TARGET_JAR%" "%APPDATA%\bolt-launcher\custom-runelite.jar" >nul 2>nul
|
||||||
|
)
|
||||||
|
|
||||||
|
color 0A
|
||||||
|
echo ========================================================
|
||||||
|
echo Bereit! Du kannst RuneLite jetzt starten.
|
||||||
|
echo ========================================================
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
Executable
+61
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Farben für die Ausgabe
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
echo -e "\n${CYAN}========================================================${NC}"
|
||||||
|
echo -e "${CYAN} RuneLite EssentialsPlus Linux Updater ${NC}"
|
||||||
|
echo -e "${CYAN}========================================================${NC}\n"
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
TARGET_JAR="$SCRIPT_DIR/RuneLite.jar"
|
||||||
|
OLD_JAR="$SCRIPT_DIR/RuneLite.jar.old"
|
||||||
|
TMP_JAR="$SCRIPT_DIR/RuneLite.jar.tmp"
|
||||||
|
DOWNLOAD_URL="https://gapspace.de/Hikyu/Runelite-EssentialsPlus/raw/branch/master/RuneLite.jar"
|
||||||
|
|
||||||
|
# 1. Bestehende Version sichern
|
||||||
|
echo -e "${YELLOW}[1/3] Sichere bestehende Version...${NC}"
|
||||||
|
if [ -f "$TARGET_JAR" ]; then
|
||||||
|
rm -f "$OLD_JAR"
|
||||||
|
mv "$TARGET_JAR" "$OLD_JAR"
|
||||||
|
echo -e " -> Alte Version gesichert als: ${CYAN}RuneLite.jar.old${NC}"
|
||||||
|
else
|
||||||
|
echo -e " -> Keine vorherige RuneLite.jar gefunden (Neuinstallation)."
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 2. Neueste Version herunterladen
|
||||||
|
echo -e "${YELLOW}[2/3] Lade neueste Version herunter...${NC}"
|
||||||
|
echo -e " Quelle: ${CYAN}$DOWNLOAD_URL${NC}\n"
|
||||||
|
|
||||||
|
rm -f "$TMP_JAR"
|
||||||
|
if ! curl -# -L -f -o "$TMP_JAR" "$DOWNLOAD_URL"; then
|
||||||
|
echo -e "\n${RED}[!] FEHLER: Download fehlgeschlagen!${NC}"
|
||||||
|
if [ -f "$OLD_JAR" ]; then
|
||||||
|
echo -e " Stelle alte Version wieder her..."
|
||||||
|
mv "$OLD_JAR" "$TARGET_JAR"
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mv "$TMP_JAR" "$TARGET_JAR"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 3. Fertigstellung & optionaler Bolt-Launcher-Sync
|
||||||
|
echo -e "${YELLOW}[3/3] Aktualisierung erfolgreich abgeschlossen!${NC}"
|
||||||
|
echo -e " -> Datei: ${GREEN}$TARGET_JAR${NC}"
|
||||||
|
|
||||||
|
BOLT_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/bolt-launcher"
|
||||||
|
if [ -d "$BOLT_DIR" ]; then
|
||||||
|
cp "$TARGET_JAR" "$BOLT_DIR/custom-runelite.jar"
|
||||||
|
echo -e " -> Bolt Launcher synchronisiert: ${GREEN}$BOLT_DIR/custom-runelite.jar${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\n${GREEN}========================================================${NC}"
|
||||||
|
echo -e "${GREEN} Bereit! Du kannst RuneLite jetzt starten. ${NC}"
|
||||||
|
echo -e "${GREEN}========================================================${NC}\n"
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
|
maven {
|
||||||
|
url = 'https://repo.runelite.net'
|
||||||
|
content {
|
||||||
|
includeGroupByRegex("net\\.runelite.*")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
def runeLiteVersion = 'latest.release'
|
||||||
|
def pluginMainClass = 'com.example.ExamplePluginTest'
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly group: 'net.runelite', name: 'client', version: runeLiteVersion
|
||||||
|
|
||||||
|
compileOnly 'org.projectlombok:lombok:1.18.30'
|
||||||
|
annotationProcessor 'org.projectlombok:lombok:1.18.30'
|
||||||
|
|
||||||
|
testImplementation 'junit:junit:4.12'
|
||||||
|
testImplementation group: 'net.runelite', name: 'client', version: runeLiteVersion
|
||||||
|
testImplementation group: 'net.runelite', name: 'jshell', version: runeLiteVersion
|
||||||
|
testCompileOnly 'org.projectlombok:lombok:1.18.30'
|
||||||
|
testAnnotationProcessor 'org.projectlombok:lombok:1.18.30'
|
||||||
|
}
|
||||||
|
|
||||||
|
group = 'com.example'
|
||||||
|
version = '1.0-SNAPSHOT'
|
||||||
|
|
||||||
|
tasks.withType(JavaCompile).configureEach {
|
||||||
|
options.encoding = 'UTF-8'
|
||||||
|
options.release.set(11)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register('run', JavaExec) {
|
||||||
|
classpath = sourceSets.test.runtimeClasspath
|
||||||
|
mainClass = pluginMainClass
|
||||||
|
|
||||||
|
jvmArgs "-ea"
|
||||||
|
args "--developer-mode", "--debug"
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register('shadowJar', Jar) {
|
||||||
|
dependsOn configurations.testRuntimeClasspath
|
||||||
|
manifest {
|
||||||
|
attributes('Main-Class': pluginMainClass, 'Multi-Release': true)
|
||||||
|
}
|
||||||
|
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
|
from sourceSets.main.output
|
||||||
|
from sourceSets.test.output
|
||||||
|
from {
|
||||||
|
configurations.testRuntimeClasspath.collect { file ->
|
||||||
|
file.isDirectory() ? file : zipTree(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exclude 'META-INF/INDEX.LIST'
|
||||||
|
exclude 'META-INF/*.SF'
|
||||||
|
exclude 'META-INF/*.DSA'
|
||||||
|
exclude 'META-INF/*.RSA'
|
||||||
|
exclude '**/module-info.class'
|
||||||
|
|
||||||
|
group = BasePlugin.BUILD_GROUP
|
||||||
|
archiveClassifier.set('shadow')
|
||||||
|
archiveFileName.set("${rootProject.name}-${project.version}-all.jar")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register('deploy') {
|
||||||
|
dependsOn 'shadowJar'
|
||||||
|
doLast {
|
||||||
|
def destDir = new File("${System.getProperty('user.home')}/.local/share/bolt-launcher")
|
||||||
|
destDir.mkdirs()
|
||||||
|
def srcFile = file("build/libs/${rootProject.name}-${project.version}-all.jar")
|
||||||
|
def targetFile = new File(destDir, "custom-runelite.jar")
|
||||||
|
def tmpFile = new File(destDir, "custom-runelite.jar.tmp")
|
||||||
|
|
||||||
|
// Copy to temporary file first
|
||||||
|
tmpFile.bytes = srcFile.bytes
|
||||||
|
|
||||||
|
// Atomic move (keeps open file descriptors valid on running Linux processes)
|
||||||
|
java.nio.file.Files.move(
|
||||||
|
tmpFile.toPath(),
|
||||||
|
targetFile.toPath(),
|
||||||
|
java.nio.file.StandardCopyOption.ATOMIC_MOVE,
|
||||||
|
java.nio.file.StandardCopyOption.REPLACE_EXISTING
|
||||||
|
)
|
||||||
|
// Also copy to Downloads as documented in CLAUDE.md
|
||||||
|
def downloadsDir = new File("${System.getProperty('user.home')}/Downloads")
|
||||||
|
if (downloadsDir.exists()) {
|
||||||
|
new File(downloadsDir, "RuneLite.jar").bytes = srcFile.bytes
|
||||||
|
}
|
||||||
|
println "Plugin sicher und atomar nach ~/.local/share/bolt-launcher/custom-runelite.jar installiert!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Vendored
BIN
Binary file not shown.
+8
@@ -0,0 +1,8 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionSha256Sum=682b4df7fe5accdca84a4d1ef6a3a6ab096b3efd5edf7de2bd8c758d95a93703
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-all.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||||
|
' "$PWD" ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
Vendored
+94
@@ -0,0 +1,94 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
+136
@@ -0,0 +1,136 @@
|
|||||||
|
# Item charges – Taskbeschreibung
|
||||||
|
|
||||||
|
Implementiere einen **„Item charges"**-Helper in **Essentials Plus** (`com.example.essentialsplus.helpers`): Auf dem **Icon jedes Items** mit bekanntem Charge/Füllstand wird eine **Zahl** gerendert. Manche Items zeigen **mehrere Zahlen** (z. B. Toxic Blowpipe: Scales + Darts). Togglebar per Checkbox, anzeige in Inventar, Ausrüstung und (optional) Bank.
|
||||||
|
|
||||||
|
**Beispiele:**
|
||||||
|
- Toxic Blowpipe (mit Scales) → `scales`-Zahl; mit geladenen Darts → zweiter Wert (Darts-Marker, s. Hinweise).
|
||||||
|
- Dizan's Quiver → zwei Zahlen: Ammo (`CHARGES_DIZANAS_QUIVER_QUANTITY`) + Blessing (`CHARGES_DIZANAS_QUIVER_BLESSING`).
|
||||||
|
- Trident/Shadow/Tome/Bowfa etc. → eine Zahl.
|
||||||
|
- **Lootingsack**: Geldwert (Coins) als Zahl auf dem Sack-Icon.
|
||||||
|
- **Coal Bag**: Füllstand via Chat-Tracking + Persistenz (kein Varbit!).
|
||||||
|
|
||||||
|
**Format:** Ganzzahlen / Stack-Format (Geld: `QuantityFormatter.quantityToStackSize`, z. B. `1.2M`). Unbekannter Wert → `?`.
|
||||||
|
|
||||||
|
## Datenquellen & Mechanik
|
||||||
|
|
||||||
|
### 1. `CHARGES_*`-Varbits (Hauptquelle, charakterweit)
|
||||||
|
|
||||||
|
Werte via `client.getVarbitValue(id)` (`net.runelite.api.gameval.VarbitID`). Mapping Item-Gruppe → Varbit (verifiziert, Release 1.12.39). Varbit aus der **ID-Variante** des Items ableiten (s. Quelle 2), nicht aus einem Katalog — zwei identische Waffen haben nur EINEN Varbit-Wert (OSRS-Grenze!).
|
||||||
|
|
||||||
|
| Item-Gruppe (ItemID-Konstanten) | VarbitID-Konstante |
|
||||||
|
|---|---|
|
||||||
|
| `TOXIC_BLOWPIPE`, `TOXIC_BLOWPIPE_LOADED`, `TOXIC_BLOWPIPE_ORNAMENT`, `TOXIC_BLOWPIPE_LOADED_ORNAMENT` | `CHARGES_TOXIC_BLOWPIPE_QUANTITY` |
|
||||||
|
| `SERPENTINE_HELM`, `SERPENTINE_HELM_CHARGED`, `_CYAN`, `_CYAN_CHARGED`, `_RED`, `_RED_CHARGED` | `CHARGES_SERPENTINE_HELM_QUANTITY` |
|
||||||
|
| `WARPED_SCEPTRE`, `WARPED_SCEPTRE_UNCHARGED` | `CHARGES_WARPED_SCEPTRE_QUANTITY` |
|
||||||
|
| `TUMEKENS_SHADOW`, `TUMEKENS_SHADOW_UNCHARGED`, `TUMEKENS_HEKA`, `TUMEKENS_HEKA_UNCHARGED` | `CHARGES_TUMEKENS_SHADOW_QUANTITY` |
|
||||||
|
| `SANGUINESTI_STAFF`, `SANGUINESTI_STAFF_UNCHARGED`, `_OR`, `_UNCHARGED_OR` | `CHARGES_SANGUINESTI_STAFF_QUANTITY` |
|
||||||
|
| `TRIDENT_OF_THE_SEAS` (+ `_E`), jew. gecharge/un-gecharge-Varianten | `CHARGES_TRIDENT_OF_THE_SEAS_QUANTITY` bzw. `..._E_QUANTITY` |
|
||||||
|
| `TOXIC_STAFF_OF_THE_DEAD` (+ `_UNCHARGED`, `_OR`-Varianten) | `CHARGES_TOXIC_STAFF_OF_THE_DEAD_QUANTITY` |
|
||||||
|
| `BOW_OF_FAERDHINEN`, `BOW_OF_FAERDHINEN_INACTIVE`, `_INFINITE*` | `CHARGES_BOW_OF_FAERDHINEN_QUANTITY` (+ `CHARGES_BOW_OF_FAERDHINEN_CORRUPTION` für korrupte Variante) |
|
||||||
|
| `BLADE_OF_SAELDOR`, `BLADE_OF_SAELDOR_INACTIVE`, `_INFINITE` | `CHARGES_BLADE_OF_SAELDOR_QUANTITY` (+ `_CORRUPTION`) |
|
||||||
|
| `VENATOR_BOW`, `VENATOR_BOW_UNCHARGED`, `VENATOR_BOW_ORNAMENT(_UNCHARGED)` | `CHARGES_VENATOR_BOW_QUANTITY` |
|
||||||
|
| `TONALZTICS_OF_RALOS_CHARGED`, `_UNCHARGED` | `CHARGES_TONALZTICS_OF_RALOS_QUANTITY` |
|
||||||
|
| `ARCLIGHT` | `CHARGES_ARCLIGHT_QUANTITY` |
|
||||||
|
| `DIZANAS_QUIVER_CHARGED`, `_UNCHARGED`, `_INFINITE`, `_BROKEN`, Trouver-Varianten | `CHARGES_DIZANAS_QUIVER_QUANTITY` (Ammo) + `CHARGES_DIZANAS_QUIVER_BLESSING` (Blessing) — zwei Zahlen |
|
||||||
|
| `TOME_OF_FIRE`, `TOME_OF_FIRE_UNCHARGED` | `CHARGES_TOME_OF_FIRE_QUANTITY` |
|
||||||
|
| `TOME_OF_WATER`, `TOME_OF_WATER_UNCHARGED` | `CHARGES_TOME_OF_WATER_QUANTITY` |
|
||||||
|
| `TOME_OF_EARTH`, `TOME_OF_EARTH_UNCHARGED` | `CHARGES_TOME_OF_EARTH_QUANTITY` |
|
||||||
|
| `ASH_SANCTIFIER` | `CHARGES_ASH_SANCTIFIER_QUANTITY` |
|
||||||
|
| `BONECRUSHER` | `CHARGES_BONECRUSHER_QUANTITY` |
|
||||||
|
| `BRYOPHYTAS_STAFF` | `CHARGES_BRYOPHYTAS_STAFF_QUANTITY` |
|
||||||
|
| `CIRCLET_OF_WATER` | `CHARGES_CIRCLET_OF_WATER_QUANTITY` |
|
||||||
|
| `SOUL_BEARER` | `CHARGES_SOUL_BEARER_QUANTITY` |
|
||||||
|
| `CELESTIAL_RING`, `CELESTIAL_RING_CHARGED` | `CHARGES_CELESTIAL_RING_QUANTITY` |
|
||||||
|
| `WILD_CAVE_SCEPTRE_CHARGED`, `WILD_CAVE_SCEPTRE_UNCHARGED` | `CHARGES_WILDERNESS_WEAPON_QUANTITY` |
|
||||||
|
| `BRACELET_OF_ETHEREUM` | `CHARGES_BRACELET_OF_ETHEREUM_QUANTITY` |
|
||||||
|
| `RING_OF_SUFFERING` | `CHARGES_RING_OF_SUFFERING_QUANTITY` |
|
||||||
|
| `XERIC_TALISMAN`, `XERIC_TALISMAN_EMPTY` | `CHARGES_XERICS_TALISMAN_QUANTITY` |
|
||||||
|
| `RING_OF_SHADOWS`, `RING_OF_SHADOWS_UNCHARGED` | `CHARGES_RING_OF_SHADOWS_QUANTITY` |
|
||||||
|
| `ECHO_BOOTS` | `CHARGES_ECHO_BOOTS_QUANTITY` |
|
||||||
|
| `AMULET_OF_BLOOD_FURY` | `CHARGES_AMULET_OF_BLOOD_FURY_QUANTITY` |
|
||||||
|
| `GIANTSOUL_AMULET_CHARGED`, `_UNCHARGED` | `CHARGES_GIANTSOUL_AMULET_QUANTITY` |
|
||||||
|
| `EYE_OF_AYAK`, `EYE_OF_AYAK_UNCHARGED` | `CHARGES_EYE_OF_AYAK_QUANTITY` |
|
||||||
|
| `HORN_OF_PLENTY`, `_UNCHARGED`, `SAILORS_AMULET`, `_EMPTY`, `COWBELL_AMULET`, `_EMPTY`, `PENDANT_OF_ATES`, `_EMPTY` | jew. `CHARGES_*_QUANTITY`-Varbit |
|
||||||
|
| Crystal Armour/Tools (Crystal, `CRYSTAL_*` piece IDs) | `CHARGES_CRYSTAL_ARMOUR_QUANTITY`, `CHARGES_CRYSTAL_TOOLS_QUANTITY`, `CHARGES_CRYSTAL_WEAK_QUANTITY` |
|
||||||
|
| `ALCHEMIST_RING` | `CHARGES_ALCHEMIST_AMULET_QUANTITY` |
|
||||||
|
|
||||||
|
Vollständige Liste ergänzen per `javap -constants net.runelite.api.gameval.VarbitID | grep 'CHARGES_.*_QUANTITY'` gegen das runelite-api-Jar (release 1.12.39; Pfad s. **Referenzen**). Das sind ~55 Konstanten; es kommen auch Solo-/Sailing-Charges vor (`CHARGES_PLANK_SACK*`, `CHARGES_BOWSTRING_SPOOL*`), nur relevante übernehmen. Stichproben-Hinweis: `DBTableID$Charges$Row` (in runelite-api `net.runelite.api.gameval.DBTableID`) listet dieselben Charge-Typen als DB-Rows (Referenz/Querschnitt, nicht zwingend zur Laufzeit nötig).
|
||||||
|
|
||||||
|
Varianten-Ableitung für die Tabelle: Einträge in `net.runelite.api.gameval.ItemID` mit typischen Suffixen (`_CHARGED`, `_UNCHARGED`, `_INACTIVE`, `_EMPTY`, `_LOADED`, `_OR`, `_CYAN/_RED`, `_TROUVER`, `_INFINITE`, `_BROKEN`) — per `javap -constants ... ItemID | grep` verifizieren.
|
||||||
|
|
||||||
|
### 2. Item-ID-Statusvarianten (Ladungs-Status)
|
||||||
|
|
||||||
|
Viele Items wechseln die **Item-ID** bei leer/laden — mehrere exakte IDs auf dieselbe Charge abbilden bzw. „0"-Semantik:
|
||||||
|
|
||||||
|
- `TOXIC_BLOWPIPE` (12924) vs `TOXIC_BLOWPIPE_LOADED` (12926): **Darts geladen: ja/nein** (zweiter Wert, Marker; die exakte **Dart-Anzahl ist NICHT in der API** — kein Varbit/Container). `DRYGORE_BLOWPIPE` vs `DRYGORE_BLOWPIPE_LOADED` analog.
|
||||||
|
- `BOW_OF_FAERDHINEN_INACTIVE` → 0; `BLADE_OF_SAELDOR_INACTIVE` → 0; `*_INFINITE` → „∞".
|
||||||
|
- Alle `*_UNCHARGED`/`*_EMPTY` → 0 bzw. kein Varbit abfragen.
|
||||||
|
|
||||||
|
### 3. Container (Echtzeit; per GameTick gelesen)
|
||||||
|
|
||||||
|
- **Lootingsack**: `client.getItemContainer(InventoryID.LOOTING_BAG)` (auch `LOOTING_BAG_OPEN`). Geldwert = Σ qty von `ItemID.COINS` im Container. (Plugin hat bereits `readContainer()`/WealthStore für Loot — hier eigenes Lesen im Overlay-Tick, da das Overlay nicht an den Container-Event gebunden ist.)
|
||||||
|
- **Dizan's Quiver Ammo**: `InventoryID.DIZANAS_QUIVER_AMMO` als Crosscheck/Geld-Anzeige zum Varbit-Wert möglich.
|
||||||
|
- **Coal/Gem/Herb sack**: kein Container, kein Varbit (verifiziert) → Quelle 4.
|
||||||
|
|
||||||
|
### 4. Coal Bag — Chat-Tracking + Persistenz
|
||||||
|
|
||||||
|
- Kein Varbit/Container in `VarbitID`/`InventoryID` (verifiziert; nur `ItemID.COAL_BAG`/`COAL_BAG_OPEN`).
|
||||||
|
- Füllstand (max **27**) ableiten aus **Chat-Nachrichten** + **letzten bekannten Wert pro Account** in `configManager` persistieren (key `coalBag.<accountKey>`, Wert int 0–27; `accountKey`-Konvention wie bestehende Persistenz im Plugin).
|
||||||
|
- Zu verarbeitende Muster (ChatMessage-Filter `Game`): Check-/Fill-Feedback „[…]`X` coal in your coal bag]" o. Ä. *exakt in-game verifizieren* (2024 wurde Feedback von Popups auf Chat umgestellt); zusätzlich Empty → 0; bei unbekannter Nachricht/Login ohne Check → **`?`** anzeigen statt falscher Zahl.
|
||||||
|
- `ChatMessage`-Events: Fehler-tolerant parsen (Regex), bei Update sofort speichern. Beim `LOGOUT` aktuellen Wert speichern. Optional Rechtsklick „Coal bag vergessen" → Wert nicht aus Chat-Nachrichten ableiten.
|
||||||
|
|
||||||
|
## Neue Dateien
|
||||||
|
|
||||||
|
### `helpers/ItemCharges.java` (`@Singleton`, Logik)
|
||||||
|
|
||||||
|
- **Datenmodell**: `ChargeValue { String text; Color color; }` (+ optional 2. Wert). Lookup-API `List<ChargeValue> chargesFor(int itemId)`:
|
||||||
|
1. exakte `itemId` in Tabelle (Load/Empty/Inactive-Varianten); dann
|
||||||
|
2. `itemManager.canonicalize(itemId)` (Noten/Placeholder) erneut prüfen.
|
||||||
|
- **`CHARGE_MAP`**: `Map<Integer itemId, ChargeSource>` statisch aufgebaut aus den ItemID-Konstanten (Quelle 1+2); `ChargeSource` = Varbit-Konstante bzw. `COAL_BAG`-Typ bzw. `LOOTING_BAG`-Container-Typ.
|
||||||
|
- **State**: Coal-Bag-Count pro Account + `Map<Integer, ...>`-Cache nur so weit nötig (Varbit-Lesen ist billig; kein harter Cache zwingend).
|
||||||
|
- **Events** (im Plugin registriert, Muster `TreeCooldowns`):
|
||||||
|
- `GameTick` → die zwei Bag-Varbits/Container aktualisieren.
|
||||||
|
- `VarbitChanged` → relevante Varbit-Id → Value merken.
|
||||||
|
- `ChatMessage` → Coal-Bag-Messages parsen (s. o.), speichern.
|
||||||
|
- `GameStateChanged` → `!= LOGGED_IN` → `reset()`, Coal-Bag-Count aus `configManager` laden bzw. auf „?" zurücksetzen.
|
||||||
|
- **Reset**: Coal-Bag-Cache auf `?` beim Logout (Persistenz bleibt).
|
||||||
|
|
||||||
|
### `helpers/ItemChargesOverlay.java` (`WidgetItemOverlay`)
|
||||||
|
|
||||||
|
- `showOnInventory(); showOnEquipment(); showOnBank();` — Bank über Config-Sub-Key steuerbar (s. unten).
|
||||||
|
- `renderItemOverlay(g, itemId, WidgetItem)`:
|
||||||
|
- `if (!config.itemCharges()) return;`
|
||||||
|
- `List<ChargeValue>` aus `chargesFor(itemId)`; leer → nichts rendern.
|
||||||
|
- Zahl unten-links am Icon (offizielles RuneLite-Muster): `bounds.x - 1, bounds.y + 15`, mit Schatten (`Fonts.RUNESCAPE_SMALL` bzw. `TimerTextSize`), Farbe aus `itemChargesColor`; **2. Wert** (Blowpipe-Darts, Quiver-Blessing) darüber (`bounds.y - 3`) in `itemChargesSecondaryColor`.
|
||||||
|
- Geld (Lootingsack): Stack-Size-Format; Low-Charge-Färbung ab `itemChargesLowAt` → `itemChargesLowColor`.
|
||||||
|
- Coins/Loot-Bag-Wert: aus `chargesFor(LOOTING_BAG_OPEN/LOOTING_BAG)`-Container-Lesung im Overlay-Tick (Falls Container null → kein Wert).
|
||||||
|
|
||||||
|
## Zu ändernde Dateien
|
||||||
|
|
||||||
|
- **`EssentialsPlusConfig.java`**: `@ConfigItem itemCharges` (bool, default `false`, position ~37). Optionen als `configManager`-Keys via `setHelperOption` (Muster wie treeCooldowns):
|
||||||
|
- `itemChargesWeapons` (bool, true) — Varbit-Charges an/aus
|
||||||
|
- `itemChargesBags` (bool, true) — Loot-Bag-Geld + Coal-Bag-Füllstand an/aus
|
||||||
|
- `itemChargesBank` (bool, false) — Overlay auch in Bank (Achtung Hinweise!)
|
||||||
|
- `itemChargesSize` (`TimerTextSize`, SMALL), `itemChargesColor` (Color, weiß), `itemChargesSecondaryColor` (Color, hellgrau `0xC0C0C0`), `itemChargesLowColor` (Color, rot `0xFF3333`), `itemChargesLowAt` (int, 0 = aus)
|
||||||
|
- **`EssentialsPlusPlugin.java`**: `@Inject ItemCharges` + `ItemChargesOverlay`; `startUp()`: `eventBus.register(itemCharges)`, `overlayManager.add(itemChargesOverlay)`; `shutDown()`: spiegeln + `itemCharges.reset()`. `isHelperOption()`: `case "itemCharges"` → `config.itemCharges()`. `setHelperOption`/`onConfigChanged`: Bestände für `itemcharges*`-Keys refresh (Muster solcher Bestands-Abschnitte).
|
||||||
|
- **`HelpersCard.java`**: neues `JCheckBox "Item charges"` (Tooltip: „Charge-Zahl auf Item-Icons (Blowpipe, Trident, Tome, …) + Loot-Bag-Geld & Coal-Bag-Füllstand. Rechtsklick für Optionen"). Rechtsklick-Menü analog `styleMenu`/`treeMenu`: Toggles `itemChargesWeapons`/`itemChargesBags`/`itemChargesBank`, Größe, Farben, Schwellenwert, Reset. `refresh()` erweitern.
|
||||||
|
|
||||||
|
## Verifikation
|
||||||
|
|
||||||
|
- `./gradlew compileJava` (bzw. `./gradlew deploy` für Bolt).
|
||||||
|
- Manuell: Blowpipe mit Scales (nur Scales-Zahl) → mit Darts laden (2. Zahl erscheint); Trident/Shadow/Sang/Tome (Zahl); Lootingsack mit Coins betreten (Geld auf Sack-Icon); Coal Bag füllen → Zahl steigt, Empty → 0, Relog → Wert bleibt; Bank öffnen mit `itemChargesBank` → Zahlen auch dort; beide Blowpipes (leer + geladen) im Inventar → korrekte per-ID-Darstellung.
|
||||||
|
|
||||||
|
## Hinweise / Einschränkungen
|
||||||
|
|
||||||
|
- `CHARGES_*`-Varbits sind **charakterweit, nicht pro Item-Exemplar**: zwei unterschiedlich geladene Tridents zeigen denselben Wert (OSRS-Systemgrenze, deckungsgleich mit dem In-Game-Charge-Overlay).
|
||||||
|
- **Bank-Anzeige** dementsprechend unzuverlässig für „unbenutzte" Kopien; default daher `itemChargesBank = false`.
|
||||||
|
- Coal Bag: nur Heuristik (Chat + Persistenz), kein API-Wert; nach Login unbekannt → `?`.
|
||||||
|
- Blowpipe: Dart-**Anzahl** nicht in der API → 2. Wert ist Darts-geladen-Marker, keine Zahl.
|
||||||
|
- Keine Chat-Popup-abhängigkeit (Popup = ist abgeschaltet hat), ausschließlich ChatMessage-Events verwenden.
|
||||||
|
|
||||||
|
## Referenzen
|
||||||
|
|
||||||
|
- `tree-cooldown.md` (gleiche Struktur), `reflect.txt`/Cachierte Jars für Konstanten:
|
||||||
|
- runelite-api: `/home/hikyu/.gradle/caches/modules-2/files-2.1/net.runelite/runelite-api/1.12.39/<hash>/runelite-api-1.12.39.jar` → `net/runelite/api/gameval/{VarbitID,ItemID,InventoryID,DBTableID$Charges$Row}`
|
||||||
|
- client: `/home/hikyu/.gradle/caches/modules-2/files-2.1/net.runelite/client/1.12.39/<hash>/client-1.12.39.jar` → `net.runelite.client.ui.overlay.WidgetItemOverlay`, `components.TextComponent`, `ui.FontManager`
|
||||||
|
- Konventionen: `helpers/TreeCooldowns.java` (Logik+Events), `helpers/TreeCooldownOverlay.java`/`ItemPriceOverlay.java` (Overlay-Zeichnung), `HelpersCard.java` (Checkbox+Menü), `EssentialsPlusPlugin.isHelperOption/setHelperOption/onConfigChanged`.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
displayName=Essentials Plus
|
||||||
|
author=hikyu
|
||||||
|
description=Essentials Plus Plugin Suite
|
||||||
|
tags=essentials,plus,wealth
|
||||||
|
version=1.0.0
|
||||||
|
plugins=com.example.essentialsplus.EssentialsPlusPlugin
|
||||||
|
build=standard
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
rootProject.name = 'bank-highlighter'
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# Skill "Reset Hourly" – Taskbeschreibung
|
||||||
|
|
||||||
|
Implementiere ein **Rechtsklick-Feature „Reset Hourly"** in **Essentials Plus**. Es erscheint an zwei Stellen:
|
||||||
|
|
||||||
|
1. **In-Game-XP-HUD** (`skills/SkillsHudOverlay.java`): Rechtsklick auf das Overlay zeigt den Menüeintrag „Reset Hourly".
|
||||||
|
2. **Skills-Karte** im Seitenpanel: auf der **Overview-Karte** als Eintrag im bestehenden Karten-Rechtsklick-Menü und im **Skills-Tab** als eigenes Rechtsklick-Popup auf der Karte.
|
||||||
|
|
||||||
|
Klick setzt die **aktuell angezeigte XP-Zählung** zurück. Es wirkt **nur**, wenn der global aktive Zeitraum `Session` oder `Now` ist. Alle anderen Zeiträume (`1H`, `1D`, `2W`, `3M`, `All`) bleiben unberührt – insbesondere bleibt der **Gesamt-XP-Wert (`All`) erhalten**, weil er aus der ersten Sample-Spur abgeleitet wird.
|
||||||
|
|
||||||
|
**Display-Sprache:** Deutsch (Menütext trotzdem exakt `Reset Hourly`).
|
||||||
|
|
||||||
|
## Hintergrund / Datenmodell
|
||||||
|
|
||||||
|
- `skills/SkillHistory.java` speichert **absolute** XP-Samples pro Skill mit Zeitstempel (`XpSample`, `List<XpSample> samples`), ein File pro Charakter (JSON in `essentials-plus/skills`).
|
||||||
|
- `ui/TimeFilter.java` + `ui/TimeRange.java` liefern den global geteilten Zeitraum: `CURRENT("Now")`, `SESSION("Sess")`, `HOUR("1H")`, `DAY`, `TWO_WEEKS`, `QUARTER`, `ALL`.
|
||||||
|
- Gewinne werden als `latest - at(start)` berechnet; der Start kommt aus `SkillHistory.start(filter)`, `starts(filter)` und `gained(filter)`.
|
||||||
|
- Es gibt **noch kein** Reset-/Baseline-Feld. Der Reset wird als **Zeitmarker** umgesetzt, nicht durch Löschen von Samples.
|
||||||
|
|
||||||
|
## Neue Dateien
|
||||||
|
|
||||||
|
### `skills/SkillHistory.java` (erweitern, keine neue Datei)
|
||||||
|
|
||||||
|
- Zwei persistente Marker ergänzen (0 = nie zurückgesetzt):
|
||||||
|
- `public long resetSessionAt;`
|
||||||
|
- `public long resetNowAt;`
|
||||||
|
- `public synchronized void reset(TimeRange range)`:
|
||||||
|
- `SESSION` → `resetSessionAt = System.currentTimeMillis()`
|
||||||
|
- `CURRENT` → `resetNowAt = System.currentTimeMillis()`
|
||||||
|
- sonst → no-op
|
||||||
|
- `dirty = true` (Persistenz-Flag, existiert bereits)
|
||||||
|
- Startlogik **nur** für die betroffenen Ranges clampen:
|
||||||
|
- `starts(TimeFilter)`: bei `SESSION` für jeden Skill `Math.max(filter.start(), resetSessionAt)`; bei `CURRENT` pro Skill `Math.max(currentStart(i, now), resetNowAt)`, idle (`Long.MAX_VALUE`) bleibt idle.
|
||||||
|
- `start(TimeFilter)`: bei `SESSION` `Math.max(filter.start(), resetSessionAt)`; bei `CURRENT` nach `currentStart(-1, now)` clampen (Fallback `now` wenn idle).
|
||||||
|
- `gained(TimeFilter)`: Nicht-Current-Zweig von `gained(filter.start())` auf `gained(start(filter))` umstellen, damit der Session-Clamp greift. Current-Zweig nutzt bereits `starts(filter)`.
|
||||||
|
- Da `range(long)`, `levelUps(long)` und `activeMillis(long, int)` ihren Start aus `start(filter)` bekommen, ziehen Charts, Stats/„XP per hour" und Level-Ups automatisch mit.
|
||||||
|
- `normalize()` prüft nur `skills`/`samples`; die neuen Felder brauchen keine Migration (fehlend = 0 = nie zurückgesetzt).
|
||||||
|
|
||||||
|
### `skills/SkillStore.java` (erweitern)
|
||||||
|
|
||||||
|
- `public void resetHourly(String key, TimeRange range)`: History per `histories.get(key)` holen; wenn vorhanden `h.reset(range)` und `save(h)` aufrufen.
|
||||||
|
- Wird vom Plugin auf dem Executor aufgerufen (File-I/O), nicht auf dem Swing-/Client-Thread.
|
||||||
|
|
||||||
|
## Zu ändernde Dateien
|
||||||
|
|
||||||
|
### `skills/SkillsHudOverlay.java`
|
||||||
|
|
||||||
|
- Im Konstruktor: `addMenuEntry(net.runelite.api.MenuAction.RUNELITE_OVERLAY, "Reset Hourly", "Skills")`.
|
||||||
|
- Die Render-Logik nutzt bereits `history.starts(filter)`/`gained(filter)` → greift den Reset automatisch, **kein** Render-Umbau nötig.
|
||||||
|
- Verifikation der API (RuneLite 1.12.39): `Overlay.addMenuEntry(MenuAction, option, target)` und die 4-arg-Variante mit `Consumer<MenuEntry>` existieren; `OverlayRenderer` injiziert den Callback in `MenuEntry.onClick(...)` **und** feuert `OverlayMenuClicked`.
|
||||||
|
|
||||||
|
### `EssentialsPlusPlugin.java`
|
||||||
|
|
||||||
|
- Import `net.runelite.client.events.OverlayMenuClicked` (+ `net.runelite.client.ui.overlay.Overlay` falls nötig).
|
||||||
|
- `public void resetSkillsHourly(String key)`:
|
||||||
|
- `if (key == null) return;`
|
||||||
|
- `executor.execute(() -> skillStore.resetHourly(key, timeFilter.getRange()));`
|
||||||
|
- danach Panel-Refresh: `EssentialsPlusPanel p = panel; if (p != null) p.refreshSkillsLater();`
|
||||||
|
- `@Subscribe public void onOverlayMenuClicked(OverlayMenuClicked event)`:
|
||||||
|
- wenn `event.getOverlay() == skillsHudOverlay` und `"Reset Hourly".equals(event.getEntry().getOption())` → `resetSkillsHourly(skillKey)`.
|
||||||
|
- `getSkillStore()`, `getSkillKey()`, `getTimeFilter()` sind vorhandene Lombok-Getter; `skillsHudOverlay` ist injiziert (`EssentialsPlusPlugin.java:232`).
|
||||||
|
- Alternativ den `Consumer<MenuEntry>` der 4-arg-`addMenuEntry` nutzen; dann ist kein `OverlayMenuClicked`-Subscriber nötig (beides funktioniert). Empfehlung: Subscriber, damit die Aktion im Plugin kapselt bleibt.
|
||||||
|
|
||||||
|
### `ui/EssentialsPlusPanel.java` (Overview-Karte)
|
||||||
|
|
||||||
|
- In `menu(String id)` im `case SKILLS` (ca. Zeile 560) einen Eintrag ergänzen:
|
||||||
|
- `JMenuItem reset = new JMenuItem("Reset Hourly");`
|
||||||
|
- Action: Character über `SkillsPanel.resolve(plugin.getSkillStore(), null, plugin.getSkillKey())` auflösen; wenn nicht null → `plugin.resetSkillsHourly(h.key);` und danach `refreshSkills();`.
|
||||||
|
- **Kein** zweites Popup auf der Overview-Karte installieren: `CardDrag` (`ui/CardDrag.java`) konsumiert dort bereits Rechtsklick und öffnet `menu(id)` – sonst gäbe es zwei Menüs.
|
||||||
|
|
||||||
|
### `ui/SkillsPanel.java` (Skills-Tab)
|
||||||
|
|
||||||
|
- Nach `private final SkillsCard card = new SkillsCard(null);` (Zeile 61) im Konstruktor einen `MouseAdapter` auf `card` installieren:
|
||||||
|
- bei `SwingUtilities.isRightMouseButton(e)` (oder `e.isPopupTrigger()`) ein `JPopupMenu` mit `JMenuItem "Reset Hourly"` bauen und `menu.show(card, e.getX(), e.getY())` zeigen.
|
||||||
|
- Action: `plugin.resetSkillsHourly(shownKey);` danach `refresh();` (bzw. `refreshLater()`).
|
||||||
|
- Im Skills-Tab ist **keine** `CardDrag` installiert, daher kollidiert das Popup nicht.
|
||||||
|
|
||||||
|
## Verifikation
|
||||||
|
|
||||||
|
- `./gradlew test` (bestehende Tests + neuer Test unten) und `./gradlew build`.
|
||||||
|
- Neuer Test in `src/test/java/com/example/essentialsplus/SkillHistoryTest.java` (Muster: `record` per Reflection, package-private):
|
||||||
|
- Samples über ca. eine Stunde anlegen, dann `h.reset(TimeRange.CURRENT)` bzw. `h.reset(TimeRange.SESSION)`.
|
||||||
|
- Erwartung: `gained(filter)` für den betroffenen Range startet neu (Baseline = Sample bei `resetAt`); `gained(TimeRange.ALL)` bleibt unverändert; `1H`/`1D` bleiben unverändert.
|
||||||
|
- Manuell:
|
||||||
|
- Filter auf **Sess** oder **Now** stellen → Rechtsklick aufs XP-HUD → „Reset Hourly" → `XP gained` zählt ab 0 neu; Gesamt-XP (`All`) unverändert.
|
||||||
|
- Filter auf `1H`/`1D`/`All` stellen → „Reset Hourly" ändert die Anzeige nicht.
|
||||||
|
- Overview-Skills-Karte rechtsklicken → „Reset Hourly"; Skills-Tab-Karte rechtsklicken → „Reset Hourly".
|
||||||
|
- Nach Reset Datei `essentials-plus/skills/<key>.json` prüfen (`resetSessionAt`/`resetNowAt` gesetzt); Relog → Session wird ohnehin vom neuen Login übersteuert, `Now`-Marker bleibt gespeichert.
|
||||||
|
|
||||||
|
## Hinweise / Einschränkungen
|
||||||
|
|
||||||
|
- „Reset Hourly" wirkt bewusst **nur** auf `Session` und `Now`; bei `1H`/`1D`/`2W`/`3M`/`All` passiert nichts (`TimeRange`-Vergleich im `reset(...)`).
|
||||||
|
- Der Gesamtwert (`All`) bleibt in jedem Fall erhalten, da er aus der ersten Sample-Spur (`samples.get(0)`) berechnet wird und der Reset nur einen Start-Clamp setzt.
|
||||||
|
- Marker sind pro Charakter im `SkillHistory`-JSON persistiert; ein fehlendes Feld nach älterem Stand bedeutet 0 (nie zurückgesetzt) und ist damit abwärtskompatibel.
|
||||||
|
- `record(...)` bleibt unverändert: Es fügt weiterhin Samples hinzu; der Reset löscht keine Historie.
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.example;
|
||||||
|
|
||||||
|
import net.runelite.client.config.Config;
|
||||||
|
import net.runelite.client.config.ConfigGroup;
|
||||||
|
import net.runelite.client.config.ConfigItem;
|
||||||
|
import net.runelite.client.config.ConfigSection;
|
||||||
|
|
||||||
|
@ConfigGroup("example")
|
||||||
|
public interface ExampleConfig extends Config
|
||||||
|
{
|
||||||
|
@ConfigSection(
|
||||||
|
name = "General Settings",
|
||||||
|
description = "Allgemeine Einstellungen für das Plugin",
|
||||||
|
position = 0
|
||||||
|
)
|
||||||
|
String generalSection = "generalSection";
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "greeting",
|
||||||
|
name = "Welcome Greeting",
|
||||||
|
description = "The message to show in game chat when logging in or enabling the plugin",
|
||||||
|
section = generalSection,
|
||||||
|
position = 1
|
||||||
|
)
|
||||||
|
default String greeting()
|
||||||
|
{
|
||||||
|
return "Hello World!";
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "showOnLogin",
|
||||||
|
name = "Show on Login",
|
||||||
|
description = "Whether to send the greeting message when logging in",
|
||||||
|
section = generalSection,
|
||||||
|
position = 2
|
||||||
|
)
|
||||||
|
default boolean showOnLogin()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
package com.example;
|
||||||
|
|
||||||
|
import com.example.model.Profile;
|
||||||
|
import com.example.overlay.BankHighlighterOverlay;
|
||||||
|
import com.example.overlay.ScreenFlashOverlay;
|
||||||
|
import com.example.overlay.TileHighlighterOverlay;
|
||||||
|
import com.example.overlay.WorldObjectHighlighterOverlay;
|
||||||
|
import com.example.ui.ExamplePluginPanel;
|
||||||
|
import com.google.inject.Provides;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.runelite.api.ChatMessageType;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.GameState;
|
||||||
|
import net.runelite.api.events.GameStateChanged;
|
||||||
|
import net.runelite.api.events.GameTick;
|
||||||
|
import net.runelite.api.events.ItemContainerChanged;
|
||||||
|
import net.runelite.client.callback.ClientThread;
|
||||||
|
import net.runelite.client.config.ConfigManager;
|
||||||
|
import net.runelite.client.eventbus.Subscribe;
|
||||||
|
import net.runelite.client.game.ItemManager;
|
||||||
|
import net.runelite.client.plugins.Plugin;
|
||||||
|
import net.runelite.client.plugins.PluginDescriptor;
|
||||||
|
import net.runelite.client.plugins.banktags.BankTagsService;
|
||||||
|
import net.runelite.client.ui.ClientToolbar;
|
||||||
|
import net.runelite.client.ui.NavigationButton;
|
||||||
|
import net.runelite.client.ui.overlay.OverlayManager;
|
||||||
|
import net.runelite.client.util.ColorUtil;
|
||||||
|
import net.runelite.client.util.ImageUtil;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@PluginDescriptor(
|
||||||
|
name = "Bank Highlighter / Auto-Profile",
|
||||||
|
description = "Aktiviert automatisch Profile, Item-Highlights, 3D-Objekte, Boden-Tiles und Pfade",
|
||||||
|
tags = {"bank", "highlighter", "profile", "auto", "herblore", "tiles", "path", "agility"}
|
||||||
|
)
|
||||||
|
public class ExamplePlugin extends Plugin implements ProfileManager.ProfileChangeListener
|
||||||
|
{
|
||||||
|
@Getter
|
||||||
|
@Inject
|
||||||
|
private Client client;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Inject
|
||||||
|
private ClientThread clientThread;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Inject
|
||||||
|
private ItemManager itemManager;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Nullable
|
||||||
|
@com.google.inject.Inject(optional = true)
|
||||||
|
private BankTagsService bankTagsService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private ConfigManager configManager;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private ClientToolbar clientToolbar;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private OverlayManager overlayManager;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private ProfileManager profileManager;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private BankHighlighterOverlay bankHighlighterOverlay;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private WorldObjectHighlighterOverlay worldObjectHighlighterOverlay;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private TileHighlighterOverlay tileHighlighterOverlay;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private ScreenFlashOverlay screenFlashOverlay;
|
||||||
|
|
||||||
|
private NavigationButton navButton;
|
||||||
|
private ExamplePluginPanel panel;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void startUp() throws Exception
|
||||||
|
{
|
||||||
|
log.info("Bank Highlighter / Auto-Profile started!");
|
||||||
|
|
||||||
|
profileManager.addListener(this);
|
||||||
|
panel = new ExamplePluginPanel(this, profileManager);
|
||||||
|
|
||||||
|
final BufferedImage icon = ImageUtil.loadImageResource(getClass(), "/com/example/icon.png");
|
||||||
|
|
||||||
|
navButton = NavigationButton.builder()
|
||||||
|
.tooltip("Bank Highlighter & Profile")
|
||||||
|
.icon(icon)
|
||||||
|
.priority(5)
|
||||||
|
.panel(panel)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
clientToolbar.addNavigation(navButton);
|
||||||
|
overlayManager.add(bankHighlighterOverlay);
|
||||||
|
overlayManager.add(worldObjectHighlighterOverlay);
|
||||||
|
overlayManager.add(tileHighlighterOverlay);
|
||||||
|
overlayManager.add(screenFlashOverlay);
|
||||||
|
|
||||||
|
clientThread.invokeLater(() ->
|
||||||
|
{
|
||||||
|
if (client.getGameState() == GameState.LOGGED_IN)
|
||||||
|
{
|
||||||
|
profileManager.evaluate(client, itemManager, bankTagsService);
|
||||||
|
panel.updateLiveInspector(client);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void shutDown() throws Exception
|
||||||
|
{
|
||||||
|
log.info("Bank Highlighter / Auto-Profile stopped!");
|
||||||
|
profileManager.removeListener(this);
|
||||||
|
|
||||||
|
overlayManager.remove(bankHighlighterOverlay);
|
||||||
|
overlayManager.remove(worldObjectHighlighterOverlay);
|
||||||
|
overlayManager.remove(tileHighlighterOverlay);
|
||||||
|
overlayManager.remove(screenFlashOverlay);
|
||||||
|
|
||||||
|
if (navButton != null)
|
||||||
|
{
|
||||||
|
clientToolbar.removeNavigation(navButton);
|
||||||
|
navButton = null;
|
||||||
|
}
|
||||||
|
panel = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onGameTick(GameTick event)
|
||||||
|
{
|
||||||
|
if (client.getGameState() == GameState.LOGGED_IN)
|
||||||
|
{
|
||||||
|
screenFlashOverlay.decrementTick();
|
||||||
|
profileManager.evaluate(client, itemManager, bankTagsService);
|
||||||
|
if (panel != null)
|
||||||
|
{
|
||||||
|
panel.updateLiveInspector(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onItemContainerChanged(ItemContainerChanged event)
|
||||||
|
{
|
||||||
|
if (client.getGameState() == GameState.LOGGED_IN)
|
||||||
|
{
|
||||||
|
profileManager.evaluate(client, itemManager, bankTagsService);
|
||||||
|
if (panel != null)
|
||||||
|
{
|
||||||
|
panel.updateLiveInspector(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onGameStateChanged(GameStateChanged gameStateChanged)
|
||||||
|
{
|
||||||
|
if (gameStateChanged.getGameState() == GameState.LOGGED_IN)
|
||||||
|
{
|
||||||
|
clientThread.invokeLater(() ->
|
||||||
|
{
|
||||||
|
profileManager.evaluate(client, itemManager, bankTagsService);
|
||||||
|
if (panel != null)
|
||||||
|
{
|
||||||
|
panel.updateLiveInspector(client);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActiveProfileChanged(Profile oldProfile, Profile newProfile)
|
||||||
|
{
|
||||||
|
if (newProfile != null)
|
||||||
|
{
|
||||||
|
// 1. Chat Message
|
||||||
|
if (newProfile.isNotifyOnActivation())
|
||||||
|
{
|
||||||
|
String msg = newProfile.getActivationMessage().replace("{name}", newProfile.getName());
|
||||||
|
sendChatMessage(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Screen Flash
|
||||||
|
if (newProfile.isScreenFlash())
|
||||||
|
{
|
||||||
|
Color color = ColorUtil.fromString(newProfile.getFlashColor());
|
||||||
|
screenFlashOverlay.flash(color != null ? color : new Color(255, 152, 0), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Sound Effect
|
||||||
|
if (newProfile.isPlaySound())
|
||||||
|
{
|
||||||
|
clientThread.invokeLater(() ->
|
||||||
|
{
|
||||||
|
if (client.getGameState() == GameState.LOGGED_IN)
|
||||||
|
{
|
||||||
|
client.playSoundEffect(newProfile.getSoundId());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onProfileListUpdated()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendManualChatMessage(String message)
|
||||||
|
{
|
||||||
|
clientThread.invokeLater(() ->
|
||||||
|
{
|
||||||
|
if (client.getGameState() == GameState.LOGGED_IN)
|
||||||
|
{
|
||||||
|
sendChatMessage(message);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.warn("Cannot send chat message - player is not logged in");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendChatMessage(String message)
|
||||||
|
{
|
||||||
|
client.addChatMessage(ChatMessageType.GAMEMESSAGE, "", "<col=FF9800>[Profile]</col> " + message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
ExampleConfig provideConfig(ConfigManager configManager)
|
||||||
|
{
|
||||||
|
return configManager.getConfig(ExampleConfig.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,483 @@
|
|||||||
|
package com.example;
|
||||||
|
|
||||||
|
import com.example.model.Folder;
|
||||||
|
import com.example.model.Profile;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.client.config.ConfigManager;
|
||||||
|
import net.runelite.client.game.ItemManager;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Singleton
|
||||||
|
public class ProfileManager
|
||||||
|
{
|
||||||
|
private static final String CONFIG_GROUP = "bankhighlighter";
|
||||||
|
private static final String PROFILES_KEY = "profiles";
|
||||||
|
private static final String FOLDERS_KEY = "folders";
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final List<Folder> folders = new ArrayList<>();
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final List<Profile> profiles = new ArrayList<>();
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private Profile activeProfile = null;
|
||||||
|
|
||||||
|
private final ConfigManager configManager;
|
||||||
|
private final Gson gson;
|
||||||
|
|
||||||
|
public interface ProfileChangeListener
|
||||||
|
{
|
||||||
|
void onActiveProfileChanged(Profile oldProfile, Profile newProfile);
|
||||||
|
void onProfileListUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final List<ProfileChangeListener> listeners = new ArrayList<>();
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ProfileManager(ConfigManager configManager, Gson gson)
|
||||||
|
{
|
||||||
|
this.configManager = configManager;
|
||||||
|
this.gson = gson;
|
||||||
|
loadProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addListener(ProfileChangeListener listener)
|
||||||
|
{
|
||||||
|
if (!listeners.contains(listener))
|
||||||
|
{
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeListener(ProfileChangeListener listener)
|
||||||
|
{
|
||||||
|
listeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadProfiles()
|
||||||
|
{
|
||||||
|
profiles.clear();
|
||||||
|
folders.clear();
|
||||||
|
|
||||||
|
// Load Folders
|
||||||
|
String foldersJson = configManager.getConfiguration(CONFIG_GROUP, FOLDERS_KEY);
|
||||||
|
if (foldersJson != null && !foldersJson.trim().isEmpty())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Type listType = new TypeToken<List<Folder>>() {}.getType();
|
||||||
|
List<Folder> loaded = gson.fromJson(foldersJson, listType);
|
||||||
|
if (loaded != null)
|
||||||
|
{
|
||||||
|
folders.addAll(loaded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
log.error("Fehler beim Laden der Ordner aus Config", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load Profiles
|
||||||
|
String json = configManager.getConfiguration(CONFIG_GROUP, PROFILES_KEY);
|
||||||
|
if (json != null && !json.trim().isEmpty())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Type listType = new TypeToken<List<Profile>>() {}.getType();
|
||||||
|
List<Profile> loaded = gson.fromJson(json, listType);
|
||||||
|
if (loaded != null && !loaded.isEmpty())
|
||||||
|
{
|
||||||
|
profiles.addAll(loaded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
log.error("Fehler beim Laden der Profile aus Config", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profiles.isEmpty())
|
||||||
|
{
|
||||||
|
loadHerblorePrayerPotionWorkflow();
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyListUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveProfiles()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
String json = gson.toJson(profiles);
|
||||||
|
configManager.setConfiguration(CONFIG_GROUP, PROFILES_KEY, json);
|
||||||
|
|
||||||
|
String foldersJson = gson.toJson(folders);
|
||||||
|
configManager.setConfiguration(CONFIG_GROUP, FOLDERS_KEY, foldersJson);
|
||||||
|
|
||||||
|
notifyListUpdated();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
log.error("Fehler beim Speichern der Profile / Ordner", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preset 1: Herblore Prayer Potions
|
||||||
|
public void loadHerblorePrayerPotionWorkflow()
|
||||||
|
{
|
||||||
|
Folder herbFolder = new Folder("Prayer Potions", "Prayer potion(4)");
|
||||||
|
folders.add(herbFolder);
|
||||||
|
|
||||||
|
Profile step2 = new Profile("2. Inv: Grimy säubern", "Grimy ranarr weed");
|
||||||
|
step2.setFolderId(herbFolder.getId());
|
||||||
|
step2.setRequiredInventory("Grimy ranarr weed");
|
||||||
|
step2.setHighlightInBank(false);
|
||||||
|
step2.setHighlightInInventory(true);
|
||||||
|
step2.setHighlightItems("Grimy ranarr weed");
|
||||||
|
step2.setHighlightColor("#4CAF50");
|
||||||
|
step2.setShowTextLabel(true);
|
||||||
|
step2.setCustomTextLabel("Clean");
|
||||||
|
step2.setActivationMessage("Schritt 2: Grimy Ranarr im Inventar säubern!");
|
||||||
|
|
||||||
|
Profile step3 = new Profile("3. Inv: Unf Potion mixen", "Ranarr weed");
|
||||||
|
step3.setFolderId(herbFolder.getId());
|
||||||
|
step3.setRequiredInventory("Ranarr weed, Vial of water");
|
||||||
|
step3.setHighlightInBank(false);
|
||||||
|
step3.setHighlightInInventory(true);
|
||||||
|
step3.setHighlightItems("Ranarr weed, Vial of water");
|
||||||
|
step3.setHighlightColor("#FF9800");
|
||||||
|
step3.setShowTextLabel(true);
|
||||||
|
step3.setCustomTextLabel("Combine");
|
||||||
|
step3.setActivationMessage("Schritt 3: Ranarr Weed & Vial of Water kombinieren!");
|
||||||
|
|
||||||
|
Profile step5 = new Profile("5. Inv: Potion fertigstellen", "Prayer potion(3)");
|
||||||
|
step5.setFolderId(herbFolder.getId());
|
||||||
|
step5.setRequiredInventory("Ranarr potion (unf), Snape grass");
|
||||||
|
step5.setHighlightInBank(false);
|
||||||
|
step5.setHighlightInInventory(true);
|
||||||
|
step5.setHighlightItems("Ranarr potion (unf), Snape grass");
|
||||||
|
step5.setHighlightColor("#9C27B0");
|
||||||
|
step5.setShowTextLabel(true);
|
||||||
|
step5.setCustomTextLabel("Combine");
|
||||||
|
step5.setActivationMessage("Schritt 5: Snape Grass & Unf Potion zur Prayer Potion mixen!");
|
||||||
|
|
||||||
|
Profile step4a = new Profile("4a. Welt: Bank öffnen", "Ranarr potion (unf)");
|
||||||
|
step4a.setFolderId(herbFolder.getId());
|
||||||
|
step4a.setRequireBankClosed(true);
|
||||||
|
step4a.setRequiredInventory("Ranarr potion (unf)");
|
||||||
|
step4a.setEnableObjectHighlight(true);
|
||||||
|
step4a.setHighlightObjects("Bank, Bank booth, Bank chest, Banker, Grand Exchange booth");
|
||||||
|
step4a.setObjectHighlightColor("#00E5FF");
|
||||||
|
step4a.setShowObjectText(true);
|
||||||
|
step4a.setObjectCustomText("Bank öffnen");
|
||||||
|
step4a.setActivationMessage("Schritt 4a: Klicke auf die Bank, um Snape Grass zu holen!");
|
||||||
|
|
||||||
|
Profile step6a = new Profile("6a. Welt: Bank öffnen", "Prayer potion(4)");
|
||||||
|
step6a.setFolderId(herbFolder.getId());
|
||||||
|
step6a.setRequireBankClosed(true);
|
||||||
|
step6a.setRequiredInventory("Prayer potion");
|
||||||
|
step6a.setEnableObjectHighlight(true);
|
||||||
|
step6a.setHighlightObjects("Bank, Bank booth, Bank chest, Banker, Grand Exchange booth");
|
||||||
|
step6a.setObjectHighlightColor("#00E5FF");
|
||||||
|
step6a.setShowObjectText(true);
|
||||||
|
step6a.setObjectCustomText("Bank öffnen");
|
||||||
|
step6a.setActivationMessage("Schritt 6a: Klicke auf die Bank, um fertige Potions einzulagern!");
|
||||||
|
|
||||||
|
Profile step6b = new Profile("6b. Bank: Potions deponieren", "Prayer potion(4)");
|
||||||
|
step6b.setFolderId(herbFolder.getId());
|
||||||
|
step6b.setRequireBankOpen(true);
|
||||||
|
step6b.setRequiredInventory("Prayer potion");
|
||||||
|
step6b.setHighlightInBank(false);
|
||||||
|
step6b.setHighlightInInventory(true);
|
||||||
|
step6b.setHighlightItems("Prayer potion");
|
||||||
|
step6b.setHighlightColor("#FF5722");
|
||||||
|
step6b.setShowTextLabel(true);
|
||||||
|
step6b.setCustomTextLabel("Depot");
|
||||||
|
step6b.setActivationMessage("Schritt 6b: Fertige Prayer Potions in die Bank einlagern!");
|
||||||
|
|
||||||
|
Profile step4b = new Profile("4b. Bank: Snape Grass nehmen", "Snape grass");
|
||||||
|
step4b.setFolderId(herbFolder.getId());
|
||||||
|
step4b.setRequireBankOpen(true);
|
||||||
|
step4b.setRequiredInventory("Ranarr potion (unf)");
|
||||||
|
step4b.setHighlightInBank(true);
|
||||||
|
step4b.setHighlightInInventory(false);
|
||||||
|
step4b.setHighlightItems("Snape grass");
|
||||||
|
step4b.setHighlightColor("#00E5FF");
|
||||||
|
step4b.setShowTextLabel(true);
|
||||||
|
step4b.setCustomTextLabel("Nehmen");
|
||||||
|
step4b.setActivationMessage("Schritt 4b: Snape Grass aus der Bank entnehmen!");
|
||||||
|
|
||||||
|
Profile step1 = new Profile("1. Bank: Zutaten nehmen", "Vial of water");
|
||||||
|
step1.setFolderId(herbFolder.getId());
|
||||||
|
step1.setRequireBankOpen(true);
|
||||||
|
step1.setBankTag("herblore");
|
||||||
|
step1.setHighlightInBank(true);
|
||||||
|
step1.setHighlightInInventory(false);
|
||||||
|
step1.setHighlightItems("Vial of water, Grimy ranarr weed");
|
||||||
|
step1.setHighlightColor("#00E5FF");
|
||||||
|
step1.setShowTextLabel(true);
|
||||||
|
step1.setCustomTextLabel("Nehmen");
|
||||||
|
step1.setActivationMessage("Schritt 1: Vial of Water & Grimy Ranarr aus der Bank nehmen!");
|
||||||
|
|
||||||
|
profiles.add(step2);
|
||||||
|
profiles.add(step3);
|
||||||
|
profiles.add(step5);
|
||||||
|
profiles.add(step4a);
|
||||||
|
profiles.add(step6a);
|
||||||
|
profiles.add(step6b);
|
||||||
|
profiles.add(step4b);
|
||||||
|
profiles.add(step1);
|
||||||
|
|
||||||
|
saveProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preset 2: 🍳 Cooking (z. B. Raw Karambwan am Feuer/Range)
|
||||||
|
public void loadCookingKarambwanWorkflow()
|
||||||
|
{
|
||||||
|
Folder cookFolder = new Folder("Cooking: Karambwan", "Cooked karambwan");
|
||||||
|
folders.add(cookFolder);
|
||||||
|
|
||||||
|
Profile step1 = new Profile("1. Bank: Rohen Fisch nehmen", "Raw karambwan");
|
||||||
|
step1.setFolderId(cookFolder.getId());
|
||||||
|
step1.setRequireBankOpen(true);
|
||||||
|
step1.setHighlightInBank(true);
|
||||||
|
step1.setHighlightInInventory(false);
|
||||||
|
step1.setHighlightItems("Raw karambwan, Raw shark, Raw monkfish");
|
||||||
|
step1.setHighlightColor("#00E5FF");
|
||||||
|
step1.setShowTextLabel(true);
|
||||||
|
step1.setCustomTextLabel("Nehmen");
|
||||||
|
step1.setActivationMessage("Schritt 1: Rohen Fisch aus der Bank entnehmen!");
|
||||||
|
|
||||||
|
Profile step2 = new Profile("2. Welt: Am Herd/Feuer braten", "Raw karambwan");
|
||||||
|
step2.setFolderId(cookFolder.getId());
|
||||||
|
step2.setRequireBankClosed(true);
|
||||||
|
step2.setRequiredInventory("Raw karambwan");
|
||||||
|
step2.setEnableObjectHighlight(true);
|
||||||
|
step2.setHighlightObjects("Range, Stove, Fire, Cooking range, Clay oven");
|
||||||
|
step2.setObjectHighlightColor("#FF5722");
|
||||||
|
step2.setShowObjectText(true);
|
||||||
|
step2.setObjectCustomText("Braten");
|
||||||
|
step2.setActivationMessage("Schritt 2: Klicke auf den Herd/das Feuer zum Braten!");
|
||||||
|
|
||||||
|
Profile step3 = new Profile("3. Welt: Zurück zur Bank", "Cooked karambwan");
|
||||||
|
step3.setFolderId(cookFolder.getId());
|
||||||
|
step3.setRequireBankClosed(true);
|
||||||
|
step3.setRequiredInventory("Cooked karambwan");
|
||||||
|
step3.setEnableObjectHighlight(true);
|
||||||
|
step3.setHighlightObjects("Bank, Bank booth, Bank chest, Banker");
|
||||||
|
step3.setObjectHighlightColor("#00E5FF");
|
||||||
|
step3.setShowObjectText(true);
|
||||||
|
step3.setObjectCustomText("Bank öffnen");
|
||||||
|
step3.setActivationMessage("Schritt 3: Gehe zur Bank, um den gebratenen Fisch einzulagern!");
|
||||||
|
|
||||||
|
Profile step4 = new Profile("4. Bank: Fisch deponieren", "Cooked karambwan");
|
||||||
|
step4.setFolderId(cookFolder.getId());
|
||||||
|
step4.setRequireBankOpen(true);
|
||||||
|
step4.setRequiredInventory("Cooked karambwan");
|
||||||
|
step4.setHighlightInBank(false);
|
||||||
|
step4.setHighlightInInventory(true);
|
||||||
|
step4.setHighlightItems("Cooked karambwan, Burnt karambwan");
|
||||||
|
step4.setHighlightColor("#4CAF50");
|
||||||
|
step4.setShowTextLabel(true);
|
||||||
|
step4.setCustomTextLabel("Depot");
|
||||||
|
step4.setActivationMessage("Schritt 4: Gebratenen Fisch in die Bank einlagern!");
|
||||||
|
|
||||||
|
profiles.add(step2);
|
||||||
|
profiles.add(step3);
|
||||||
|
profiles.add(step4);
|
||||||
|
profiles.add(step1);
|
||||||
|
|
||||||
|
saveProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preset 3: 🏃 Agility Rooftop Course Guide (mit 3D Hindernissen + Boden-Tiles + Pfadlinien)
|
||||||
|
public void loadAgilitySeersWorkflow()
|
||||||
|
{
|
||||||
|
Folder agilFolder = new Folder("Agility: Seers Rooftop", "Graceful hood");
|
||||||
|
folders.add(agilFolder);
|
||||||
|
|
||||||
|
Profile start = new Profile("1. Start: Bank Wall hochklettern", "Graceful top");
|
||||||
|
start.setFolderId(agilFolder.getId());
|
||||||
|
start.setRegionId("10806, 10550");
|
||||||
|
start.setEnableObjectHighlight(true);
|
||||||
|
start.setHighlightObjects("Wall, Climbing wall, Rough wall");
|
||||||
|
start.setObjectHighlightColor("#2ECC71");
|
||||||
|
start.setShowObjectText(true);
|
||||||
|
start.setObjectCustomText("Klettern");
|
||||||
|
start.setEnableTileHighlight(true);
|
||||||
|
start.setHighlightTiles("2729, 3488, 0, Start-Kachel");
|
||||||
|
start.setTileHighlightColor("#2ECC71");
|
||||||
|
start.setRenderPathLine(true);
|
||||||
|
start.setActivationMessage("Agility: Starte den Seers' Rooftop Kurs an der Wand!");
|
||||||
|
|
||||||
|
Profile gap1 = new Profile("2. Rooftop: Lücke überspringen", "Graceful legs");
|
||||||
|
gap1.setFolderId(agilFolder.getId());
|
||||||
|
gap1.setEnableObjectHighlight(true);
|
||||||
|
gap1.setHighlightObjects("Gap, Tightrope, Edge");
|
||||||
|
gap1.setObjectHighlightColor("#00E5FF");
|
||||||
|
gap1.setShowObjectText(true);
|
||||||
|
gap1.setObjectCustomText("Springen");
|
||||||
|
gap1.setActivationMessage("Agility: Nächstes Dach-Hindernis überqueren!");
|
||||||
|
|
||||||
|
profiles.add(gap1);
|
||||||
|
profiles.add(start);
|
||||||
|
|
||||||
|
saveProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Folder createFolder(String name, String iconItem)
|
||||||
|
{
|
||||||
|
Folder f = new Folder(name, iconItem);
|
||||||
|
folders.add(f);
|
||||||
|
saveProfiles();
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteFolder(Folder folder)
|
||||||
|
{
|
||||||
|
if (folder == null) return;
|
||||||
|
for (Profile p : profiles)
|
||||||
|
{
|
||||||
|
if (folder.getId().equals(p.getFolderId()))
|
||||||
|
{
|
||||||
|
p.setFolderId("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
folders.removeIf(f -> f.getId().equals(folder.getId()));
|
||||||
|
saveProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Folder getFolderById(String id)
|
||||||
|
{
|
||||||
|
if (id == null || id.isEmpty()) return null;
|
||||||
|
for (Folder f : folders)
|
||||||
|
{
|
||||||
|
if (f.getId().equals(id)) return f;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Profile createProfile(String name)
|
||||||
|
{
|
||||||
|
Profile p = new Profile(name);
|
||||||
|
profiles.add(p);
|
||||||
|
saveProfiles();
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteProfile(Profile profile)
|
||||||
|
{
|
||||||
|
if (profile == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeProfile != null && activeProfile.getId().equals(profile.getId()))
|
||||||
|
{
|
||||||
|
setActiveProfile(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
profiles.removeIf(p -> p.getId().equals(profile.getId()));
|
||||||
|
saveProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateProfile(Profile updated)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < profiles.size(); i++)
|
||||||
|
{
|
||||||
|
if (profiles.get(i).getId().equals(updated.getId()))
|
||||||
|
{
|
||||||
|
profiles.set(i, updated);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saveProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void moveProfileUp(Profile profile)
|
||||||
|
{
|
||||||
|
int idx = profiles.indexOf(profile);
|
||||||
|
if (idx > 0)
|
||||||
|
{
|
||||||
|
Collections.swap(profiles, idx, idx - 1);
|
||||||
|
saveProfiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void moveProfileDown(Profile profile)
|
||||||
|
{
|
||||||
|
int idx = profiles.indexOf(profile);
|
||||||
|
if (idx >= 0 && idx < profiles.size() - 1)
|
||||||
|
{
|
||||||
|
Collections.swap(profiles, idx, idx + 1);
|
||||||
|
saveProfiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void evaluate(Client client, ItemManager itemManager, net.runelite.client.plugins.banktags.BankTagsService bankTagsService)
|
||||||
|
{
|
||||||
|
if (client == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Profile matched = null;
|
||||||
|
for (Profile p : profiles)
|
||||||
|
{
|
||||||
|
if (!p.isEnabled())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.getFolderId() != null && !p.getFolderId().isEmpty())
|
||||||
|
{
|
||||||
|
Folder f = getFolderById(p.getFolderId());
|
||||||
|
if (f != null && !f.isEnabled())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.matches(client, itemManager, bankTagsService))
|
||||||
|
{
|
||||||
|
matched = p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matched != activeProfile)
|
||||||
|
{
|
||||||
|
setActiveProfile(matched);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setActiveProfile(Profile newActive)
|
||||||
|
{
|
||||||
|
Profile old = this.activeProfile;
|
||||||
|
this.activeProfile = newActive;
|
||||||
|
for (ProfileChangeListener listener : listeners)
|
||||||
|
{
|
||||||
|
listener.onActiveProfileChanged(old, newActive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyListUpdated()
|
||||||
|
{
|
||||||
|
for (ProfileChangeListener listener : listeners)
|
||||||
|
{
|
||||||
|
listener.onProfileListUpdated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,587 @@
|
|||||||
|
package com.example.essentialsplus;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.helpers.TimerTextSize;
|
||||||
|
import java.awt.Color;
|
||||||
|
import net.runelite.client.config.Config;
|
||||||
|
import net.runelite.client.config.ConfigGroup;
|
||||||
|
import net.runelite.client.config.ConfigItem;
|
||||||
|
import net.runelite.client.config.Range;
|
||||||
|
import net.runelite.client.config.Units;
|
||||||
|
|
||||||
|
@ConfigGroup(EssentialsPlusConfig.GROUP)
|
||||||
|
public interface EssentialsPlusConfig extends Config
|
||||||
|
{
|
||||||
|
String GROUP = "essentialsplus";
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "snapshotSeconds",
|
||||||
|
name = "Min. Abstand Datenpunkte",
|
||||||
|
description = "Mindestabstand zwischen zwei gespeicherten Datenpunkten. Unveraenderte Werte werden nie doppelt gespeichert.",
|
||||||
|
position = 1
|
||||||
|
)
|
||||||
|
@Units(Units.SECONDS)
|
||||||
|
@Range(min = 10, max = 3600)
|
||||||
|
default int snapshotSeconds()
|
||||||
|
{
|
||||||
|
return 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "rawDays",
|
||||||
|
name = "Jede Aenderung behalten (Tage)",
|
||||||
|
description = "So lange wird jeder einzelne Datenpunkt gespeichert. Danach: stuendliche Werte (Open/Close/Min/Max/Durchschnitt).",
|
||||||
|
position = 2
|
||||||
|
)
|
||||||
|
@Units(" d")
|
||||||
|
@Range(min = 1, max = 90)
|
||||||
|
default int rawDays()
|
||||||
|
{
|
||||||
|
return 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "hourlyDays",
|
||||||
|
name = "Stuendliche Werte (Tage)",
|
||||||
|
description = "So lange bleiben stuendliche Werte erhalten. Aeltere Daten werden zu Tageswerten (Open/Close/Min/Max/Durchschnitt).",
|
||||||
|
position = 3
|
||||||
|
)
|
||||||
|
@Units(" d")
|
||||||
|
@Range(min = 1, max = 3650)
|
||||||
|
default int hourlyDays()
|
||||||
|
{
|
||||||
|
return 90;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "includeGe",
|
||||||
|
name = "GE-Angebote zaehlen",
|
||||||
|
description = "Gold und Items in laufenden Grand Exchange Angeboten mitzaehlen.",
|
||||||
|
position = 4
|
||||||
|
)
|
||||||
|
default boolean includeGe()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "revalueOffline",
|
||||||
|
name = "Offline-Charaktere neu bewerten",
|
||||||
|
description = "Bewertet ausgeloggte Charaktere stuendlich mit aktuellen GE-Preisen neu (Marktschwankungen im Verlauf).",
|
||||||
|
position = 5
|
||||||
|
)
|
||||||
|
default boolean revalueOffline()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "showGeBuyLimits",
|
||||||
|
name = "GE-Kauflimit (4h) im Interface",
|
||||||
|
description = "Zeigt im Grand Exchange direkt das 4-Stunden-Kauflimit, bereits gekaufte Menge und verbleibendes Limit an.",
|
||||||
|
position = 7
|
||||||
|
)
|
||||||
|
default boolean showGeBuyLimits()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "showSkillsHud",
|
||||||
|
name = "XP-HUD im Spiel",
|
||||||
|
description = "Zeigt ingame die gefarmte XP pro Skill im gewaehlten Zeitraum (1H, 1D, ...) an.",
|
||||||
|
position = 8
|
||||||
|
)
|
||||||
|
default boolean showSkillsHud()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "skillsHudMax",
|
||||||
|
name = "XP-HUD max. Skills",
|
||||||
|
description = "Maximale Anzahl Skills im XP-HUD (meiste XP zuerst).",
|
||||||
|
position = 9
|
||||||
|
)
|
||||||
|
@Range(min = 1, max = 24)
|
||||||
|
default int skillsHudMax()
|
||||||
|
{
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "skillsHudLevel",
|
||||||
|
name = "XP-HUD: Level",
|
||||||
|
description = "Zeigt das aktuelle Level pro Skill im XP-HUD.",
|
||||||
|
position = 10
|
||||||
|
)
|
||||||
|
default boolean skillsHudLevel()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "skillsHudPercent",
|
||||||
|
name = "XP-HUD: Fortschritt %",
|
||||||
|
description = "Zeigt den Fortschritt im aktuellen Level in Prozent.",
|
||||||
|
position = 11
|
||||||
|
)
|
||||||
|
default boolean skillsHudPercent()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "skillsHudXpLeft",
|
||||||
|
name = "XP-HUD: XP bis Level",
|
||||||
|
description = "Zeigt die verbleibende XP bis zum naechsten Level.",
|
||||||
|
position = 12
|
||||||
|
)
|
||||||
|
default boolean skillsHudXpLeft()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "skillsHudTimeToLevel",
|
||||||
|
name = "XP-HUD: Zeit bis Level",
|
||||||
|
description = "Geschaetzte Zeit bis zum naechsten Level (basierend auf XP/h im gewaehlten Zeitraum).",
|
||||||
|
position = 13
|
||||||
|
)
|
||||||
|
default boolean skillsHudTimeToLevel()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "skillsHudXpHour",
|
||||||
|
name = "XP-HUD: XP/h",
|
||||||
|
description = "Zeigt XP pro Stunde aktiver Trainingszeit im gewaehlten Zeitraum.",
|
||||||
|
position = 14
|
||||||
|
)
|
||||||
|
default boolean skillsHudXpHour()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "skillsHudProgressBar",
|
||||||
|
name = "XP-HUD: Fortschrittsbalken",
|
||||||
|
description = "Zeigt einen Balken mit dem Fortschritt zum naechsten Level.",
|
||||||
|
position = 15
|
||||||
|
)
|
||||||
|
default boolean skillsHudProgressBar()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "wikiMenuOption",
|
||||||
|
name = "Wiki im Rechtsklick-Menue",
|
||||||
|
description = "Fuegt beim Rechtsklick auf NPCs, Objekte und Items eine 'Wiki'-Option hinzu, die die Seite im Spiel oeffnet.",
|
||||||
|
position = 20
|
||||||
|
)
|
||||||
|
default boolean wikiMenuOption()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "attackTimer",
|
||||||
|
name = "Attack Timer",
|
||||||
|
description = "Zeigt ueber dem eigenen Kopf, wie viele Ticks bis zum naechsten Angriff verbleiben (0 = Angriff).",
|
||||||
|
position = 30
|
||||||
|
)
|
||||||
|
default boolean attackTimer()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "enemyAttackTimer",
|
||||||
|
name = "Enemy Attack Timer",
|
||||||
|
description = "Zeigt ueber NPCs, die dich angreifen, die Ticks bis zu ihrem naechsten Angriff. Die Angriffsgeschwindigkeit wird aus ihren Angriffen gelernt (Standard 4).",
|
||||||
|
position = 31
|
||||||
|
)
|
||||||
|
default boolean enemyAttackTimer()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "enemyAttackStyle",
|
||||||
|
name = "Enemy Attack Style",
|
||||||
|
description = "Zeigt ueber NPCs, die dich angreifen, womit sie zuletzt angegriffen haben (Nahkampf, Fernkampf, Magie).",
|
||||||
|
position = 32
|
||||||
|
)
|
||||||
|
default boolean enemyAttackStyle()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "itemPrices",
|
||||||
|
name = "Item Preise",
|
||||||
|
description = "Zeigt beim Hovern ueber Inventar-Items eine Zeile mit GE- und/oder HA-Preis.",
|
||||||
|
position = 33
|
||||||
|
)
|
||||||
|
default boolean itemPrices()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "itemPricesGe",
|
||||||
|
name = "Item Preise: GE",
|
||||||
|
description = "GE-Preis in der Hover-Zeile anzeigen.",
|
||||||
|
position = 34
|
||||||
|
)
|
||||||
|
default boolean itemPricesGe()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "itemPricesHa",
|
||||||
|
name = "Item Preise: HA",
|
||||||
|
description = "High-Alch-Wert in der Hover-Zeile anzeigen.",
|
||||||
|
position = 35
|
||||||
|
)
|
||||||
|
default boolean itemPricesHa()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "treeCooldowns",
|
||||||
|
name = "Tree Cooldowns",
|
||||||
|
description = "Zeigt Respawn-Countdown auf gefaellten Baeumen und verbleibende Hack-Zeit auf langlebigen Baeumen.",
|
||||||
|
position = 36
|
||||||
|
)
|
||||||
|
default boolean treeCooldowns()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "itemCharges",
|
||||||
|
name = "Item charges",
|
||||||
|
description = "Zeigt Ladungen und Füllstände direkt auf Item-Icons an (Blowpipe, Trident, Tome, Quiver, Loot-Bag, Coal-Bag).",
|
||||||
|
position = 37
|
||||||
|
)
|
||||||
|
default boolean itemCharges()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "enemyAttackStyleText",
|
||||||
|
name = "Enemy Attack Style: Text statt Icon",
|
||||||
|
description = "Zeigt 'Melee', 'Range' oder 'Mage' statt des Gebets-Icons.",
|
||||||
|
position = 60
|
||||||
|
)
|
||||||
|
default boolean enemyAttackStyleText()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "enemyAttackStyleSize",
|
||||||
|
name = "Enemy Attack Style: Groesse",
|
||||||
|
description = "Groesse von Icon bzw. Text.",
|
||||||
|
position = 61
|
||||||
|
)
|
||||||
|
default TimerTextSize enemyAttackStyleSize()
|
||||||
|
{
|
||||||
|
return TimerTextSize.NORMAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "enemyAttackStyleOffset",
|
||||||
|
name = "Enemy Attack Style: Hoehe",
|
||||||
|
description = "Abstand ueber dem Kopf, unabhaengig vom Enemy Attack Timer.",
|
||||||
|
position = 62
|
||||||
|
)
|
||||||
|
@Range(min = -100, max = 300)
|
||||||
|
default int enemyAttackStyleOffset()
|
||||||
|
{
|
||||||
|
return 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "attackTimerBar",
|
||||||
|
name = "Attack Timer: Tick-Balken",
|
||||||
|
description = "Zeigt unter der Zahl einen Balken mit einem Kaestchen pro Tick.",
|
||||||
|
position = 40
|
||||||
|
)
|
||||||
|
default boolean attackTimerBar()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "attackTimerSize",
|
||||||
|
name = "Attack Timer: Textgroesse",
|
||||||
|
description = "Groesse der Zahl.",
|
||||||
|
position = 41
|
||||||
|
)
|
||||||
|
default TimerTextSize attackTimerSize()
|
||||||
|
{
|
||||||
|
return TimerTextSize.NORMAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "attackTimerColor",
|
||||||
|
name = "Attack Timer: Farbe",
|
||||||
|
description = "Farbe der Zahl und des Balkens.",
|
||||||
|
position = 42
|
||||||
|
)
|
||||||
|
default Color attackTimerColor()
|
||||||
|
{
|
||||||
|
return Color.WHITE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "attackTimerReadyColor",
|
||||||
|
name = "Attack Timer: Farbe bei 0",
|
||||||
|
description = "Farbe, wenn der Angriff faellig ist (0).",
|
||||||
|
position = 43
|
||||||
|
)
|
||||||
|
default Color attackTimerReadyColor()
|
||||||
|
{
|
||||||
|
return new Color(0x66FF66);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "attackTimerOffset",
|
||||||
|
name = "Attack Timer: Hoehe",
|
||||||
|
description = "Abstand ueber dem Kopf.",
|
||||||
|
position = 44
|
||||||
|
)
|
||||||
|
@Range(min = -100, max = 200)
|
||||||
|
default int attackTimerOffset()
|
||||||
|
{
|
||||||
|
return 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "enemyAttackTimerBar",
|
||||||
|
name = "Enemy Attack Timer: Tick-Balken",
|
||||||
|
description = "Zeigt unter der Zahl einen Balken mit einem Kaestchen pro Tick.",
|
||||||
|
position = 50
|
||||||
|
)
|
||||||
|
default boolean enemyAttackTimerBar()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "enemyAttackTimerSize",
|
||||||
|
name = "Enemy Attack Timer: Textgroesse",
|
||||||
|
description = "Groesse der Zahl.",
|
||||||
|
position = 51
|
||||||
|
)
|
||||||
|
default TimerTextSize enemyAttackTimerSize()
|
||||||
|
{
|
||||||
|
return TimerTextSize.NORMAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "enemyAttackTimerColor",
|
||||||
|
name = "Enemy Attack Timer: Farbe",
|
||||||
|
description = "Farbe der Zahl und des Balkens.",
|
||||||
|
position = 52
|
||||||
|
)
|
||||||
|
default Color enemyAttackTimerColor()
|
||||||
|
{
|
||||||
|
return new Color(0xFF7043);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "enemyAttackTimerReadyColor",
|
||||||
|
name = "Enemy Attack Timer: Farbe bei 0",
|
||||||
|
description = "Farbe, wenn der Angriff faellig ist (0).",
|
||||||
|
position = 53
|
||||||
|
)
|
||||||
|
default Color enemyAttackTimerReadyColor()
|
||||||
|
{
|
||||||
|
return new Color(0x66FF66);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "enemyAttackTimerOffset",
|
||||||
|
name = "Enemy Attack Timer: Hoehe",
|
||||||
|
description = "Abstand ueber dem Kopf.",
|
||||||
|
position = 54
|
||||||
|
)
|
||||||
|
@Range(min = -100, max = 200)
|
||||||
|
default int enemyAttackTimerOffset()
|
||||||
|
{
|
||||||
|
return 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "treeCooldownRespawn",
|
||||||
|
name = "Tree Cooldowns: Respawn",
|
||||||
|
description = "Respawn-Countdown ueber Baumstuempfen anzeigen.",
|
||||||
|
position = 70
|
||||||
|
)
|
||||||
|
default boolean treeCooldownRespawn()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "treeCooldownChop",
|
||||||
|
name = "Tree Cooldowns: Hackzeit",
|
||||||
|
description = "Verbleibende Hack-Zeit ueber Baeumen waehrend des Faellens anzeigen.",
|
||||||
|
position = 71
|
||||||
|
)
|
||||||
|
default boolean treeCooldownChop()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "treeCooldownBar",
|
||||||
|
name = "Tree Cooldowns: Fortschrittsbalken",
|
||||||
|
description = "Zeigt einen kleinen Fortschrittsbalken unter der Sekundenzahl.",
|
||||||
|
position = 72
|
||||||
|
)
|
||||||
|
default boolean treeCooldownBar()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "treeCooldownSize",
|
||||||
|
name = "Tree Cooldowns: Textgroesse",
|
||||||
|
description = "Groesse der Zahl.",
|
||||||
|
position = 73
|
||||||
|
)
|
||||||
|
default TimerTextSize treeCooldownSize()
|
||||||
|
{
|
||||||
|
return TimerTextSize.NORMAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "treeCooldownColor",
|
||||||
|
name = "Tree Cooldowns: Farbe",
|
||||||
|
description = "Farbe der Zahl.",
|
||||||
|
position = 74
|
||||||
|
)
|
||||||
|
default Color treeCooldownColor()
|
||||||
|
{
|
||||||
|
return new Color(0x8FBF3F);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "treeCooldownReadyColor",
|
||||||
|
name = "Tree Cooldowns: Farbe bei Bereit",
|
||||||
|
description = "Farbe kurz vor bzw. beim Respawn.",
|
||||||
|
position = 75
|
||||||
|
)
|
||||||
|
default Color treeCooldownReadyColor()
|
||||||
|
{
|
||||||
|
return new Color(0x66FF66);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "treeCooldownOffset",
|
||||||
|
name = "Tree Cooldowns: Hoehe",
|
||||||
|
description = "Abstand ueber dem Stumpf bzw. der Baum-Basis.",
|
||||||
|
position = 76
|
||||||
|
)
|
||||||
|
@Range(min = -100, max = 200)
|
||||||
|
default int treeCooldownOffset()
|
||||||
|
{
|
||||||
|
return 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "itemChargesWeapons",
|
||||||
|
name = "Item Charges: Waffen & Rüstung",
|
||||||
|
description = "Ladungen von Waffen und Rüstungsteilen anzeigen.",
|
||||||
|
position = 80
|
||||||
|
)
|
||||||
|
default boolean itemChargesWeapons()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "itemChargesBags",
|
||||||
|
name = "Item Charges: Beutel & Säcke",
|
||||||
|
description = "Geldwert im Looting Bag und Füllstand des Coal Bags anzeigen.",
|
||||||
|
position = 81
|
||||||
|
)
|
||||||
|
default boolean itemChargesBags()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "itemChargesBank",
|
||||||
|
name = "Item Charges: Bank",
|
||||||
|
description = "Ladungen auch auf Items im Bank-Interface anzeigen (Achtung: Varbits sind charakterweit).",
|
||||||
|
position = 82
|
||||||
|
)
|
||||||
|
default boolean itemChargesBank()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "itemChargesSize",
|
||||||
|
name = "Item Charges: Textgröße",
|
||||||
|
description = "Schriftgröße der Ladungsanzeige.",
|
||||||
|
position = 83
|
||||||
|
)
|
||||||
|
default TimerTextSize itemChargesSize()
|
||||||
|
{
|
||||||
|
return TimerTextSize.SMALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "itemChargesColor",
|
||||||
|
name = "Item Charges: Farbe",
|
||||||
|
description = "Farbe der primären Ladungszahl.",
|
||||||
|
position = 84
|
||||||
|
)
|
||||||
|
default Color itemChargesColor()
|
||||||
|
{
|
||||||
|
return Color.WHITE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "itemChargesSecondaryColor",
|
||||||
|
name = "Item Charges: Sekundärfarbe",
|
||||||
|
description = "Farbe für Sekundärwerte (z. B. Quiver-Blessing, Darts-Marker).",
|
||||||
|
position = 85
|
||||||
|
)
|
||||||
|
default Color itemChargesSecondaryColor()
|
||||||
|
{
|
||||||
|
return new Color(0xC0C0C0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "itemChargesLowColor",
|
||||||
|
name = "Item Charges: Warnfarbe",
|
||||||
|
description = "Farbe bei niedrigem Ladungsstand.",
|
||||||
|
position = 86
|
||||||
|
)
|
||||||
|
default Color itemChargesLowColor()
|
||||||
|
{
|
||||||
|
return new Color(0xFF3333);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigItem(
|
||||||
|
keyName = "itemChargesLowAt",
|
||||||
|
name = "Item Charges: Niedrig ab",
|
||||||
|
description = "Schwellenwert für niedrige Ladungen (0 = deaktiviert).",
|
||||||
|
position = 87
|
||||||
|
)
|
||||||
|
@Range(min = 0, max = 10000)
|
||||||
|
default int itemChargesLowAt()
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,318 @@
|
|||||||
|
package com.example.essentialsplus;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.data.AccountData;
|
||||||
|
import com.example.essentialsplus.data.GeSlot;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.IntUnaryOperator;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.GrandExchangeOffer;
|
||||||
|
import net.runelite.api.GrandExchangeOfferState;
|
||||||
|
import net.runelite.api.Item;
|
||||||
|
import net.runelite.api.ItemContainer;
|
||||||
|
import net.runelite.api.gameval.InterfaceID;
|
||||||
|
import net.runelite.api.gameval.InventoryID;
|
||||||
|
import net.runelite.api.gameval.ItemID;
|
||||||
|
import net.runelite.api.widgets.Widget;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Values Grand Exchange offers without double counting.
|
||||||
|
* <p>
|
||||||
|
* A slot holds two things: the escrow of the part that has not traded yet and the collection box
|
||||||
|
* (bought items / received coins). The client does not say how much of a partially completed offer was
|
||||||
|
* already collected, so collections are detected: whenever the offer item or coins show up in inventory
|
||||||
|
* or bank while a GE / bank interface is open, that amount is treated as collected. If a slot empties and
|
||||||
|
* the items did not show up anywhere visible they went to the bank unseen and are booked there.
|
||||||
|
* The game's collection box containers are used directly when the client has them.
|
||||||
|
*/
|
||||||
|
final class GrandExchangeTracker
|
||||||
|
{
|
||||||
|
private static final int SLOTS = 8;
|
||||||
|
private static final int[] COLLECT_CONTAINERS = {
|
||||||
|
InventoryID.TRADINGPOST_SELL_0, InventoryID.TRADINGPOST_SELL_1, InventoryID.TRADINGPOST_SELL_2,
|
||||||
|
InventoryID.TRADINGPOST_SELL_3, InventoryID.TRADINGPOST_SELL_4, InventoryID.TRADINGPOST_SELL_5,
|
||||||
|
InventoryID.GE_COLLECT_6, InventoryID.GE_COLLECT_7
|
||||||
|
};
|
||||||
|
|
||||||
|
/** how long a visible coin loss may be matched to a new buy offer */
|
||||||
|
private static final long COIN_LOSS_WINDOW_MS = 15_000L;
|
||||||
|
|
||||||
|
/** inventory + visible bank holdings (canonical ids) at the previous update */
|
||||||
|
private Map<Integer, Long> previousHoldings;
|
||||||
|
/** coins that recently left inventory / visible bank and were not matched to a buy offer yet */
|
||||||
|
private long recentCoinLoss;
|
||||||
|
private long recentCoinLossAt;
|
||||||
|
|
||||||
|
void reset()
|
||||||
|
{
|
||||||
|
previousHoldings = null;
|
||||||
|
recentCoinLoss = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param live false while the offers of a fresh login have not arrived yet - persisted values are used then
|
||||||
|
* @return canonical item id -> quantity held by the Grand Exchange
|
||||||
|
*/
|
||||||
|
Map<Integer, Long> update(AccountData acc, Client client, IntUnaryOperator canonicalize, boolean live)
|
||||||
|
{
|
||||||
|
boolean migrate = acc.geSlots == null || acc.geSlots.length != SLOTS;
|
||||||
|
|
||||||
|
Map<Integer, Long> holdings = new HashMap<>();
|
||||||
|
addCanonical(holdings, acc.containers.get("inventory"), canonicalize);
|
||||||
|
addCanonical(holdings, acc.containers.get("bank"), canonicalize);
|
||||||
|
Map<Integer, Long> gains = new HashMap<>();
|
||||||
|
long updateTime = System.currentTimeMillis();
|
||||||
|
if (updateTime - recentCoinLossAt > COIN_LOSS_WINDOW_MS)
|
||||||
|
{
|
||||||
|
recentCoinLoss = 0;
|
||||||
|
}
|
||||||
|
if (previousHoldings != null)
|
||||||
|
{
|
||||||
|
long coinLoss = previousHoldings.getOrDefault(ItemID.COINS, 0L) - holdings.getOrDefault(ItemID.COINS, 0L);
|
||||||
|
if (coinLoss > 0)
|
||||||
|
{
|
||||||
|
recentCoinLoss += coinLoss;
|
||||||
|
recentCoinLossAt = updateTime;
|
||||||
|
}
|
||||||
|
for (Map.Entry<Integer, Long> e : holdings.entrySet())
|
||||||
|
{
|
||||||
|
long d = e.getValue() - previousHoldings.getOrDefault(e.getKey(), 0L);
|
||||||
|
if (d > 0)
|
||||||
|
{
|
||||||
|
gains.put(e.getKey(), d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boolean haveBaseline = previousHoldings != null;
|
||||||
|
previousHoldings = holdings;
|
||||||
|
|
||||||
|
GrandExchangeOffer[] offers = client.getGrandExchangeOffers();
|
||||||
|
Map<Integer, Long> out = new HashMap<>();
|
||||||
|
if (!live || offers == null || offers.length < SLOTS || (!haveBaseline && !migrate))
|
||||||
|
{
|
||||||
|
// keep the last known state
|
||||||
|
if (!migrate)
|
||||||
|
{
|
||||||
|
for (GeSlot s : acc.geSlots)
|
||||||
|
{
|
||||||
|
addSlot(out, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
if (migrate)
|
||||||
|
{
|
||||||
|
acc.geSlots = new GeSlot[SLOTS];
|
||||||
|
}
|
||||||
|
boolean seeded = migrate;
|
||||||
|
|
||||||
|
boolean collecting = isOpen(client, InterfaceID.GE_OFFERS) || isOpen(client, InterfaceID.GE_COLLECT) || isOpen(client, InterfaceID.BANKMAIN);
|
||||||
|
|
||||||
|
for (int i = 0; i < SLOTS; i++)
|
||||||
|
{
|
||||||
|
GrandExchangeOffer o = offers[i];
|
||||||
|
GeSlot s = acc.geSlots[i];
|
||||||
|
GrandExchangeOfferState state = o == null ? null : o.getState();
|
||||||
|
|
||||||
|
if (state == null || state == GrandExchangeOfferState.EMPTY || o.getItemId() <= 0)
|
||||||
|
{
|
||||||
|
if (s != null && !migrate)
|
||||||
|
{
|
||||||
|
// slot cleared: whatever was still collectable was collected now
|
||||||
|
int item = canonicalize.applyAsInt(s.item);
|
||||||
|
long items = s.collectItems - consume(gains, item, s.collectItems);
|
||||||
|
long coins = s.collectCoins - consume(gains, ItemID.COINS, s.collectCoins);
|
||||||
|
bookToBank(acc, item, items);
|
||||||
|
bookToBank(acc, ItemID.COINS, coins);
|
||||||
|
}
|
||||||
|
acc.geSlots[i] = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean buy = state == GrandExchangeOfferState.BUYING || state == GrandExchangeOfferState.BOUGHT
|
||||||
|
|| state == GrandExchangeOfferState.CANCELLED_BUY;
|
||||||
|
boolean cancelled = state == GrandExchangeOfferState.CANCELLED_BUY || state == GrandExchangeOfferState.CANCELLED_SELL;
|
||||||
|
long total = o.getTotalQuantity();
|
||||||
|
long price = o.getPrice();
|
||||||
|
long done = o.getQuantitySold();
|
||||||
|
long spent = o.getSpent();
|
||||||
|
|
||||||
|
// what the slot has produced so far (collectable if nothing was taken out)
|
||||||
|
long grossItems = buy ? done : (cancelled ? total - done : 0);
|
||||||
|
long grossCoins = buy ? Math.max(0, done * price - spent) + (cancelled ? (total - done) * price : 0) : spent;
|
||||||
|
long escrowItems = !buy && !cancelled ? total - done : 0;
|
||||||
|
long escrowCoins = buy && !cancelled ? (total - done) * price : 0;
|
||||||
|
|
||||||
|
if (s == null || s.item != o.getItemId() || s.buy != buy || s.total != total || s.price != price)
|
||||||
|
{
|
||||||
|
GeSlot fresh = new GeSlot();
|
||||||
|
fresh.item = o.getItemId();
|
||||||
|
fresh.buy = buy;
|
||||||
|
fresh.total = total;
|
||||||
|
fresh.price = price;
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
fresh.trackedSince = now;
|
||||||
|
fresh.placedAt = migrate ? 0 : now;
|
||||||
|
fresh.done = migrate ? done : 0;
|
||||||
|
fresh.trackedDone = fresh.done;
|
||||||
|
if (migrate && acc.geSeedCollected != null)
|
||||||
|
{
|
||||||
|
// first run with this tracker: take over what is known to be collected already
|
||||||
|
long seed = acc.geSeedCollected.getOrDefault(fresh.item, 0L);
|
||||||
|
fresh.collectedItems = Math.min(grossItems, seed);
|
||||||
|
if (seed > 0)
|
||||||
|
{
|
||||||
|
acc.geSeedCollected.put(fresh.item, seed - fresh.collectedItems);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!migrate && buy)
|
||||||
|
{
|
||||||
|
// A buy offer can be paid with coins from the bank while the bank is not visible. Whatever did not
|
||||||
|
// leave inventory / visible bank came from there - otherwise the stale bank and the offer both count it.
|
||||||
|
long cost = total * price;
|
||||||
|
long seen = Math.min(cost, recentCoinLoss);
|
||||||
|
recentCoinLoss -= seen;
|
||||||
|
bookToBank(acc, ItemID.COINS, -(cost - seen));
|
||||||
|
}
|
||||||
|
s = fresh;
|
||||||
|
acc.geSlots[i] = s;
|
||||||
|
}
|
||||||
|
|
||||||
|
int item = canonicalize.applyAsInt(s.item);
|
||||||
|
// items / coins of this slot showing up in inventory or bank were collected
|
||||||
|
if (collecting)
|
||||||
|
{
|
||||||
|
s.collectedItems += consume(gains, item, Math.max(0, grossItems - s.collectedItems));
|
||||||
|
s.collectedCoins += consume(gains, ItemID.COINS, Math.max(0, grossCoins - s.collectedCoins));
|
||||||
|
}
|
||||||
|
long collectItems = Math.max(0, grossItems - s.collectedItems);
|
||||||
|
long collectCoins = Math.max(0, grossCoins - s.collectedCoins);
|
||||||
|
ItemContainer box = client.getItemContainer(COLLECT_CONTAINERS[i]);
|
||||||
|
if (box != null)
|
||||||
|
{
|
||||||
|
long boxItems = 0;
|
||||||
|
long boxCoins = 0;
|
||||||
|
for (Item it : box.getItems())
|
||||||
|
{
|
||||||
|
if (it == null || it.getId() <= 0 || it.getQuantity() <= 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (it.getId() == ItemID.COINS)
|
||||||
|
{
|
||||||
|
boxCoins += it.getQuantity();
|
||||||
|
}
|
||||||
|
else if (canonicalize.applyAsInt(it.getId()) == item)
|
||||||
|
{
|
||||||
|
boxItems += it.getQuantity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The client can keep an outdated box after collecting and may fill it a tick after the offer, so the box
|
||||||
|
// only counts when it shrank since the last update: that part went to the bank without being seen.
|
||||||
|
long goneItems = Math.min(collectItems - boxItems, s.collectItems - boxItems);
|
||||||
|
if (goneItems > 0)
|
||||||
|
{
|
||||||
|
bookToBank(acc, item, goneItems);
|
||||||
|
s.collectedItems += goneItems;
|
||||||
|
collectItems -= goneItems;
|
||||||
|
}
|
||||||
|
long goneCoins = Math.min(collectCoins - boxCoins, s.collectCoins - boxCoins);
|
||||||
|
if (goneCoins > 0)
|
||||||
|
{
|
||||||
|
bookToBank(acc, ItemID.COINS, goneCoins);
|
||||||
|
s.collectedCoins += goneCoins;
|
||||||
|
collectCoins -= goneCoins;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
if (s.trackedSince == 0)
|
||||||
|
{
|
||||||
|
s.trackedSince = now;
|
||||||
|
s.done = done;
|
||||||
|
s.trackedDone = done;
|
||||||
|
}
|
||||||
|
if (done != s.done)
|
||||||
|
{
|
||||||
|
s.lastTradeAt = now;
|
||||||
|
}
|
||||||
|
s.done = done;
|
||||||
|
s.spent = spent;
|
||||||
|
s.state = state.name();
|
||||||
|
s.escrowItems = escrowItems;
|
||||||
|
s.escrowCoins = escrowCoins;
|
||||||
|
s.collectItems = collectItems;
|
||||||
|
s.collectCoins = collectCoins;
|
||||||
|
addSlot(out, s);
|
||||||
|
}
|
||||||
|
if (seeded)
|
||||||
|
{
|
||||||
|
acc.geSeedCollected = null;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addSlot(Map<Integer, Long> out, GeSlot s)
|
||||||
|
{
|
||||||
|
if (s == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long items = s.escrowItems + s.collectItems;
|
||||||
|
long coins = s.escrowCoins + s.collectCoins;
|
||||||
|
if (items > 0)
|
||||||
|
{
|
||||||
|
out.merge(s.item, items, Long::sum);
|
||||||
|
}
|
||||||
|
if (coins > 0)
|
||||||
|
{
|
||||||
|
out.merge(ItemID.COINS, coins, Long::sum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Takes up to {@code max} of a visible gain and returns how much was taken. */
|
||||||
|
private static long consume(Map<Integer, Long> gains, int item, long max)
|
||||||
|
{
|
||||||
|
if (max <= 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
long g = gains.getOrDefault(item, 0L);
|
||||||
|
long take = Math.min(g, max);
|
||||||
|
if (take > 0)
|
||||||
|
{
|
||||||
|
gains.put(item, g - take);
|
||||||
|
}
|
||||||
|
return take;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Books items that went to the bank while the bank was not visible. */
|
||||||
|
private static void bookToBank(AccountData acc, int item, long qty)
|
||||||
|
{
|
||||||
|
if (qty == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<Integer, Long> pending = new HashMap<>(acc.bankPending);
|
||||||
|
pending.merge(item, qty, Long::sum);
|
||||||
|
acc.bankPending = pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addCanonical(Map<Integer, Long> into, Map<Integer, Long> items, IntUnaryOperator canonicalize)
|
||||||
|
{
|
||||||
|
if (items == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (Map.Entry<Integer, Long> e : items.entrySet())
|
||||||
|
{
|
||||||
|
into.merge(canonicalize.applyAsInt(e.getKey()), e.getValue(), Long::sum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isOpen(Client client, int group)
|
||||||
|
{
|
||||||
|
Widget w = client.getWidget(group, 0);
|
||||||
|
return w != null && !w.isHidden();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.example.essentialsplus;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wealth buckets. Ordinal is used as index into persisted value arrays - only append new entries.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public enum WealthCategory
|
||||||
|
{
|
||||||
|
BANK("Bank", new Color(232, 163, 61)),
|
||||||
|
INVENTORY("Inventory", new Color(90, 176, 232)),
|
||||||
|
EQUIPMENT("Equipment", new Color(165, 123, 224)),
|
||||||
|
STORAGE("Storage", new Color(77, 201, 166)),
|
||||||
|
COFFERS("Coffers", new Color(232, 112, 122)),
|
||||||
|
GRAND_EXCHANGE("Grand Exchange", new Color(201, 214, 90)),
|
||||||
|
/** Totals imported from other plugins without a breakdown. */
|
||||||
|
IMPORTED("Imported (total only)", new Color(120, 120, 128));
|
||||||
|
|
||||||
|
public static final int COUNT = values().length;
|
||||||
|
|
||||||
|
private final String label;
|
||||||
|
private final Color color;
|
||||||
|
}
|
||||||
@@ -0,0 +1,475 @@
|
|||||||
|
package com.example.essentialsplus;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.data.AccountData;
|
||||||
|
import com.example.essentialsplus.data.ItemLedger;
|
||||||
|
import com.example.essentialsplus.data.ItemSnapshot;
|
||||||
|
import com.example.essentialsplus.data.Snapshot;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.io.Writer;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.time.DayOfWeek;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.time.temporal.TemporalAdjusters;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.runelite.client.RuneLite;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads and saves the files in ~/.runelite/essentials-plus:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code <key>.json} - state + wealth timeline</li>
|
||||||
|
* <li>{@code <key>.items.json} - item holdings over time</li>
|
||||||
|
* </ul>
|
||||||
|
* Several RuneLite clients may run at once, so files of characters that are not logged in
|
||||||
|
* in this client are re-read when they change on disk.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Singleton
|
||||||
|
public class WealthStore
|
||||||
|
{
|
||||||
|
public static final File DIR = new File(RuneLite.RUNELITE_DIR, "essentials-plus");
|
||||||
|
private static final String LEDGER_SUFFIX = ".items.json";
|
||||||
|
|
||||||
|
private final Gson gson;
|
||||||
|
private final Map<String, AccountData> accounts = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, ItemLedger> ledgers = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
WealthStore(Gson gson)
|
||||||
|
{
|
||||||
|
this.gson = gson.newBuilder().disableHtmlEscaping().create();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ accounts
|
||||||
|
|
||||||
|
public List<AccountData> all()
|
||||||
|
{
|
||||||
|
List<AccountData> list = new ArrayList<>(accounts.values());
|
||||||
|
list.sort(Comparator.comparingLong((AccountData a) -> -a.currentTotal())
|
||||||
|
.thenComparing(a -> a.name == null ? "" : a.name, String.CASE_INSENSITIVE_ORDER));
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountData get(String key)
|
||||||
|
{
|
||||||
|
return key == null ? null : accounts.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountData getOrCreate(String key, String name)
|
||||||
|
{
|
||||||
|
return accounts.computeIfAbsent(key, k ->
|
||||||
|
{
|
||||||
|
AccountData a = new AccountData();
|
||||||
|
a.key = k;
|
||||||
|
a.name = name;
|
||||||
|
return a;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void put(AccountData account)
|
||||||
|
{
|
||||||
|
accounts.put(account.key, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-reads account files changed on disk, skipping the character logged in here (this client owns it).
|
||||||
|
*
|
||||||
|
* @return true if anything changed
|
||||||
|
*/
|
||||||
|
public boolean reloadExternal(String activeKey)
|
||||||
|
{
|
||||||
|
File[] files = DIR.listFiles((d, n) -> n.endsWith(".json") && !n.endsWith(LEDGER_SUFFIX));
|
||||||
|
if (files == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean changed = false;
|
||||||
|
Set<String> seen = new HashSet<>();
|
||||||
|
for (File f : files)
|
||||||
|
{
|
||||||
|
AccountData existing = findByFile(f);
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
seen.add(existing.key);
|
||||||
|
if (existing.key.equals(activeKey) || existing.fileModified >= f.lastModified())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountData loaded = read(f, AccountData.class);
|
||||||
|
if (loaded == null || loaded.key == null || loaded.key.equals(activeKey))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
loaded.normalize();
|
||||||
|
loaded.fileModified = f.lastModified();
|
||||||
|
accounts.put(loaded.key, loaded);
|
||||||
|
seen.add(loaded.key);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// files deleted by another client
|
||||||
|
for (String key : new ArrayList<>(accounts.keySet()))
|
||||||
|
{
|
||||||
|
AccountData a = accounts.get(key);
|
||||||
|
if (a != null && !seen.contains(key) && !key.equals(activeKey) && a.fileModified > 0)
|
||||||
|
{
|
||||||
|
accounts.remove(key);
|
||||||
|
ledgers.remove(key);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Replaces the in-memory copy with the file if the file is newer (used when a character logs in). */
|
||||||
|
public AccountData refreshFromDisk(String key)
|
||||||
|
{
|
||||||
|
File f = fileFor(key);
|
||||||
|
AccountData mem = accounts.get(key);
|
||||||
|
if (f.exists() && (mem == null || mem.fileModified < f.lastModified()))
|
||||||
|
{
|
||||||
|
AccountData loaded = read(f, AccountData.class);
|
||||||
|
if (loaded != null)
|
||||||
|
{
|
||||||
|
loaded.key = key;
|
||||||
|
loaded.normalize();
|
||||||
|
loaded.fileModified = f.lastModified();
|
||||||
|
accounts.put(key, loaded);
|
||||||
|
ledgers.remove(key);
|
||||||
|
return loaded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mem;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AccountData findByFile(File f)
|
||||||
|
{
|
||||||
|
for (AccountData a : accounts.values())
|
||||||
|
{
|
||||||
|
if (fileFor(a.key).getName().equals(f.getName()))
|
||||||
|
{
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save(AccountData account)
|
||||||
|
{
|
||||||
|
if (account == null || account.key == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
File target = fileFor(account.key);
|
||||||
|
if (write(target, account))
|
||||||
|
{
|
||||||
|
account.fileModified = target.lastModified();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(String key)
|
||||||
|
{
|
||||||
|
accounts.remove(key);
|
||||||
|
ledgers.remove(key);
|
||||||
|
for (File f : new File[]{fileFor(key), ledgerFileFor(key)})
|
||||||
|
{
|
||||||
|
if (f.exists() && !f.delete())
|
||||||
|
{
|
||||||
|
log.warn("Wealth tracker: could not delete {}", f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ item ledgers
|
||||||
|
|
||||||
|
/** Item history of a character, loaded lazily and re-read when another client changed it. */
|
||||||
|
public ItemLedger ledger(String key, boolean owned)
|
||||||
|
{
|
||||||
|
ItemLedger mem = ledgers.get(key);
|
||||||
|
File f = ledgerFileFor(key);
|
||||||
|
if (mem != null && (owned || !f.exists() || mem.fileModified >= f.lastModified()))
|
||||||
|
{
|
||||||
|
return mem;
|
||||||
|
}
|
||||||
|
ItemLedger loaded = f.exists() ? read(f, ItemLedger.class) : null;
|
||||||
|
if (loaded == null)
|
||||||
|
{
|
||||||
|
loaded = mem != null ? mem : new ItemLedger();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
loaded.normalize();
|
||||||
|
loaded.fileModified = f.lastModified();
|
||||||
|
}
|
||||||
|
ledgers.put(key, loaded);
|
||||||
|
return loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveLedger(String key, ItemLedger ledger)
|
||||||
|
{
|
||||||
|
File target = ledgerFileFor(key);
|
||||||
|
if (write(target, ledger))
|
||||||
|
{
|
||||||
|
ledger.fileModified = target.lastModified();
|
||||||
|
ledger.dirty = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ io
|
||||||
|
|
||||||
|
private <T> T read(File f, Class<T> type)
|
||||||
|
{
|
||||||
|
try (Reader r = Files.newBufferedReader(f.toPath(), StandardCharsets.UTF_8))
|
||||||
|
{
|
||||||
|
return gson.fromJson(r, type);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
log.warn("Wealth tracker: could not read {}", f, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized boolean write(File target, Object data)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Files.createDirectories(DIR.toPath());
|
||||||
|
File tmp = new File(DIR, target.getName() + ".tmp");
|
||||||
|
try (Writer w = Files.newBufferedWriter(tmp.toPath(), StandardCharsets.UTF_8))
|
||||||
|
{
|
||||||
|
gson.toJson(data, w);
|
||||||
|
}
|
||||||
|
Files.move(tmp.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
log.warn("Wealth tracker: could not save {}", target, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String fileBase(String key)
|
||||||
|
{
|
||||||
|
return key.replaceAll("[^A-Za-z0-9 _()\\-]", "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
static File fileFor(String key)
|
||||||
|
{
|
||||||
|
return new File(DIR, fileBase(key) + ".json");
|
||||||
|
}
|
||||||
|
|
||||||
|
static File ledgerFileFor(String key)
|
||||||
|
{
|
||||||
|
return new File(DIR, fileBase(key) + LEDGER_SUFFIX);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ compaction
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps the timeline useful for analysis while bounding its size:
|
||||||
|
* <ul>
|
||||||
|
* <li>younger than {@code rawDays}: every change (identical consecutive points dropped)</li>
|
||||||
|
* <li>younger than {@code hourlyDays}: one point per hour</li>
|
||||||
|
* <li>older: one point per day</li>
|
||||||
|
* </ul>
|
||||||
|
* Buckets keep open, close, low, high and the time-weighted average breakdown.
|
||||||
|
* Must be called on the thread that appends to the history (client thread).
|
||||||
|
*/
|
||||||
|
public static void compact(AccountData account, int rawDays, int hourlyDays, long now)
|
||||||
|
{
|
||||||
|
List<Snapshot> src = AccountData.sanitizeHistory(account.history);
|
||||||
|
if (src.size() < 2)
|
||||||
|
{
|
||||||
|
if (src.size() != account.history.size())
|
||||||
|
{
|
||||||
|
account.history = new CopyOnWriteArrayList<>(src);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ZoneId zone = ZoneId.systemDefault();
|
||||||
|
ZonedDateTime nowZ = Instant.ofEpochMilli(now).atZone(zone);
|
||||||
|
long rawCutoff = nowZ.minusDays(rawDays).truncatedTo(ChronoUnit.HOURS).toInstant().toEpochMilli();
|
||||||
|
long hourlyCutoff = nowZ.minusDays(Math.max(rawDays, hourlyDays)).truncatedTo(ChronoUnit.DAYS).toInstant().toEpochMilli();
|
||||||
|
|
||||||
|
List<Snapshot> out = new ArrayList<>(src.size());
|
||||||
|
List<Snapshot> bucket = new ArrayList<>();
|
||||||
|
long bucketStart = Long.MIN_VALUE;
|
||||||
|
String bucketG = null;
|
||||||
|
Snapshot prevRaw = null;
|
||||||
|
|
||||||
|
for (Snapshot s : src)
|
||||||
|
{
|
||||||
|
if (s.t >= rawCutoff)
|
||||||
|
{
|
||||||
|
flush(bucket, bucketStart, bucketG, zone, out);
|
||||||
|
bucketStart = Long.MIN_VALUE;
|
||||||
|
if (!s.isAggregate() && prevRaw != null && prevRaw.sameValues(s.v))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.add(s);
|
||||||
|
prevRaw = s.isAggregate() ? null : s;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String g = s.t >= hourlyCutoff ? Snapshot.HOUR : Snapshot.DAY;
|
||||||
|
ZonedDateTime z = Instant.ofEpochMilli(s.t).atZone(zone);
|
||||||
|
long bs = (g.equals(Snapshot.HOUR) ? z.truncatedTo(ChronoUnit.HOURS) : z.truncatedTo(ChronoUnit.DAYS)).toInstant().toEpochMilli();
|
||||||
|
if (bs != bucketStart || !g.equals(bucketG))
|
||||||
|
{
|
||||||
|
flush(bucket, bucketStart, bucketG, zone, out);
|
||||||
|
bucketStart = bs;
|
||||||
|
bucketG = g;
|
||||||
|
}
|
||||||
|
bucket.add(s);
|
||||||
|
}
|
||||||
|
flush(bucket, bucketStart, bucketG, zone, out);
|
||||||
|
|
||||||
|
if (out.size() != src.size())
|
||||||
|
{
|
||||||
|
account.history = new CopyOnWriteArrayList<>(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void flush(List<Snapshot> bucket, long start, String g, ZoneId zone, List<Snapshot> out)
|
||||||
|
{
|
||||||
|
if (bucket.isEmpty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (bucket.size() == 1 && g.equals(bucket.get(0).g))
|
||||||
|
{
|
||||||
|
out.add(bucket.get(0));
|
||||||
|
bucket.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long end = g.equals(Snapshot.HOUR)
|
||||||
|
? start + Ui_HOUR
|
||||||
|
: Instant.ofEpochMilli(start).atZone(zone).plusDays(1).toInstant().toEpochMilli();
|
||||||
|
|
||||||
|
double[] sum = new double[WealthCategory.COUNT];
|
||||||
|
double weight = 0;
|
||||||
|
long lo = Long.MAX_VALUE;
|
||||||
|
long hi = Long.MIN_VALUE;
|
||||||
|
for (int i = 0; i < bucket.size(); i++)
|
||||||
|
{
|
||||||
|
Snapshot s = bucket.get(i);
|
||||||
|
long from = Math.max(s.t, start);
|
||||||
|
long to = i + 1 < bucket.size() ? bucket.get(i + 1).t : end;
|
||||||
|
double w = Math.max(1, Math.min(to, end) - from);
|
||||||
|
for (WealthCategory c : WealthCategory.values())
|
||||||
|
{
|
||||||
|
sum[c.ordinal()] += s.get(c) * w;
|
||||||
|
}
|
||||||
|
weight += w;
|
||||||
|
lo = Math.min(lo, s.low());
|
||||||
|
hi = Math.max(hi, s.high());
|
||||||
|
}
|
||||||
|
|
||||||
|
long[] avg = new long[WealthCategory.COUNT];
|
||||||
|
for (int i = 0; i < avg.length; i++)
|
||||||
|
{
|
||||||
|
avg[i] = Math.round(sum[i] / weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
Snapshot agg = new Snapshot(start + (end - start) / 2, avg);
|
||||||
|
agg.o = bucket.get(0).open();
|
||||||
|
agg.c = bucket.get(bucket.size() - 1).close();
|
||||||
|
agg.lo = lo;
|
||||||
|
agg.hi = hi;
|
||||||
|
agg.g = g;
|
||||||
|
agg.m = bucket.get(bucket.size() - 1).m;
|
||||||
|
out.add(agg);
|
||||||
|
bucket.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final long Ui_HOUR = 3_600_000L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item history resolution: hourly for 2 days, daily for 90 days, weekly afterwards.
|
||||||
|
* Identical consecutive snapshots are dropped.
|
||||||
|
*/
|
||||||
|
public static void compact(ItemLedger ledger, long now)
|
||||||
|
{
|
||||||
|
List<ItemSnapshot> src = ledger.snapshots;
|
||||||
|
if (src.size() < 2)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ZoneId zone = ZoneId.systemDefault();
|
||||||
|
List<ItemSnapshot> out = new ArrayList<>(src.size());
|
||||||
|
long prevBucket = Long.MIN_VALUE;
|
||||||
|
for (ItemSnapshot s : src)
|
||||||
|
{
|
||||||
|
long age = now - s.t;
|
||||||
|
ZonedDateTime z = Instant.ofEpochMilli(s.t).atZone(zone);
|
||||||
|
long bucket;
|
||||||
|
if (age < 2 * 86_400_000L)
|
||||||
|
{
|
||||||
|
bucket = z.truncatedTo(ChronoUnit.HOURS).toInstant().toEpochMilli();
|
||||||
|
}
|
||||||
|
else if (age < 90 * 86_400_000L)
|
||||||
|
{
|
||||||
|
bucket = z.truncatedTo(ChronoUnit.DAYS).toInstant().toEpochMilli() + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LocalDate monday = z.toLocalDate().with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
|
||||||
|
bucket = monday.atStartOfDay(zone).toInstant().toEpochMilli() + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Boolean.TRUE.equals(s.k) && age < 2 * 86_400_000L)
|
||||||
|
{
|
||||||
|
out.add(s);
|
||||||
|
prevBucket = Long.MIN_VALUE;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!out.isEmpty() && bucket == prevBucket && !Boolean.TRUE.equals(out.get(out.size() - 1).k))
|
||||||
|
{
|
||||||
|
// keep the latest state of each bucket
|
||||||
|
out.set(out.size() - 1, s);
|
||||||
|
}
|
||||||
|
else if (!out.isEmpty() && s.sameAs(out.get(out.size() - 1)))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
out.add(s);
|
||||||
|
}
|
||||||
|
prevBucket = bucket;
|
||||||
|
}
|
||||||
|
if (out.size() != src.size())
|
||||||
|
{
|
||||||
|
ledger.snapshots = new CopyOnWriteArrayList<>(out);
|
||||||
|
ledger.dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,515 @@
|
|||||||
|
package com.example.essentialsplus.collectionlog;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.runelite.api.ChatMessageType;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.EnumComposition;
|
||||||
|
import net.runelite.api.GameState;
|
||||||
|
import net.runelite.api.Item;
|
||||||
|
import net.runelite.api.ItemContainer;
|
||||||
|
import net.runelite.api.StructComposition;
|
||||||
|
import net.runelite.api.events.ChatMessage;
|
||||||
|
import net.runelite.api.gameval.InterfaceID;
|
||||||
|
import net.runelite.api.widgets.Widget;
|
||||||
|
import net.runelite.client.config.ConfigManager;
|
||||||
|
import net.runelite.client.util.Text;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collection log pages and items (read from the game cache) plus which items the logged-in account has
|
||||||
|
* obtained. Obtained items are learned whenever a collection log page is open and from the
|
||||||
|
* "New item added to your collection log" chat message, and stored per RuneScape profile.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class CollectionLogService
|
||||||
|
{
|
||||||
|
private static final String GROUP = com.example.essentialsplus.EssentialsPlusConfig.GROUP;
|
||||||
|
private static final String KEY = "clogObtained";
|
||||||
|
private static final String KEY_FULL_SYNC = "clogFullSync";
|
||||||
|
/** runs once per log item (item id, count) when the log's search is opened */
|
||||||
|
private static final int SCRIPT_SEARCH_ITEM = 4100;
|
||||||
|
|
||||||
|
/** tab structs (Bosses, Raids, Clues, Minigames, Other) */
|
||||||
|
private static final int[] TAB_STRUCTS = {471, 472, 473, 474, 475};
|
||||||
|
private static final String[] TAB_NAMES = {"Bosses", "Raids", "Clues", "Minigames", "Other"};
|
||||||
|
private static final int PARAM_TAB_PAGES = 683;
|
||||||
|
private static final int PARAM_PAGE_NAME = 689;
|
||||||
|
private static final int PARAM_PAGE_ITEMS = 690;
|
||||||
|
/** server-sent inventory holding the obtained count of every collection log item */
|
||||||
|
private static final int CLOG_INVENTORY = 620;
|
||||||
|
/** obj -> obj enum the log script uses to remap page items before looking them up in the inventory */
|
||||||
|
private static final int ENUM_ITEM_REMAP = 3721;
|
||||||
|
/** unobtained items are drawn with this transparency */
|
||||||
|
private static final int UNOBTAINED_OPACITY = 175;
|
||||||
|
private static final String NEW_ITEM = "New item added to your collection log:";
|
||||||
|
|
||||||
|
/** A collection log page, e.g. "Zulrah", with its item ids in log order. */
|
||||||
|
public static final class Page
|
||||||
|
{
|
||||||
|
public final String name;
|
||||||
|
public final List<Integer> itemIds;
|
||||||
|
|
||||||
|
Page(String name, List<Integer> itemIds)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
this.itemIds = itemIds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A collection log tab, e.g. "Bosses", with its pages in log order. */
|
||||||
|
public static final class Tab
|
||||||
|
{
|
||||||
|
public final String name;
|
||||||
|
public final List<Page> pages;
|
||||||
|
|
||||||
|
Tab(String name, List<Page> pages)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
this.pages = pages;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Client client;
|
||||||
|
private final ConfigManager configManager;
|
||||||
|
private final List<Runnable> listeners = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
|
private volatile List<Page> pages = Collections.emptyList();
|
||||||
|
private volatile List<Tab> tabs = Collections.emptyList();
|
||||||
|
private volatile Map<Integer, String> itemNames = Collections.emptyMap();
|
||||||
|
private final Set<Integer> obtained = ConcurrentHashMap.newKeySet();
|
||||||
|
private volatile String profileKey;
|
||||||
|
private volatile boolean synced;
|
||||||
|
/** when all items were last read via the log search, 0 = never */
|
||||||
|
private volatile long fullSyncAt;
|
||||||
|
/** items reported by the search script this tick: id -> count */
|
||||||
|
private final Map<Integer, Integer> searchItems = new java.util.HashMap<>();
|
||||||
|
private int searchTick = -1;
|
||||||
|
private int lastWidgetHash;
|
||||||
|
private int lastInventoryHash;
|
||||||
|
|
||||||
|
public CollectionLogService(Client client, ConfigManager configManager)
|
||||||
|
{
|
||||||
|
this.client = client;
|
||||||
|
this.configManager = configManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addListener(Runnable r)
|
||||||
|
{
|
||||||
|
listeners.add(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeListener(Runnable r)
|
||||||
|
{
|
||||||
|
listeners.remove(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client thread. Loads the page list once and the obtained items when the profile changes. */
|
||||||
|
public void onGameTick()
|
||||||
|
{
|
||||||
|
if (client.getGameState() != GameState.LOGGED_IN)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pages.isEmpty())
|
||||||
|
{
|
||||||
|
loadPages();
|
||||||
|
}
|
||||||
|
String key = configManager.getRSProfileKey();
|
||||||
|
if (key != null && !key.equals(profileKey))
|
||||||
|
{
|
||||||
|
profileKey = key;
|
||||||
|
loadObtained();
|
||||||
|
log.info("collection log: profile {} has {} stored obtained items", key, obtained.size());
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
finishSearchSync();
|
||||||
|
scanInventory();
|
||||||
|
scanOpenPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client thread. The log search reports every item with its count: a complete, exact sync. */
|
||||||
|
public void onScriptPreFired(net.runelite.api.events.ScriptPreFired event)
|
||||||
|
{
|
||||||
|
if (event.getScriptId() != SCRIPT_SEARCH_ITEM || event.getScriptEvent() == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object[] args = event.getScriptEvent().getArguments();
|
||||||
|
if (args == null || args.length < 3 || !(args[1] instanceof Integer) || !(args[2] instanceof Integer))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
searchItems.merge((Integer) args[1], (Integer) args[2], Math::max);
|
||||||
|
searchTick = client.getTickCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishSearchSync()
|
||||||
|
{
|
||||||
|
if (searchItems.isEmpty() || client.getTickCount() == searchTick || itemNames.isEmpty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<Integer, Integer> seen = new java.util.HashMap<>(searchItems);
|
||||||
|
searchItems.clear();
|
||||||
|
EnumComposition remap = client.getEnum(ENUM_ITEM_REMAP);
|
||||||
|
Set<Integer> have = new HashSet<>();
|
||||||
|
int known = 0;
|
||||||
|
for (int id : itemNames.keySet())
|
||||||
|
{
|
||||||
|
int mapped = remap.getIntValue(id);
|
||||||
|
boolean inSearch = seen.containsKey(id) || mapped > 0 && seen.containsKey(mapped);
|
||||||
|
known += inSearch ? 1 : 0;
|
||||||
|
if (seen.getOrDefault(id, 0) > 0 || mapped > 0 && seen.getOrDefault(mapped, 0) > 0)
|
||||||
|
{
|
||||||
|
have.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boolean complete = known >= itemNames.size() * 9 / 10;
|
||||||
|
log.info("collection log search: {} items reported, {} log items matched, {} obtained, complete={}", seen.size(), known, have.size(), complete);
|
||||||
|
if (complete)
|
||||||
|
{
|
||||||
|
// the search lists every item, so this replaces what was guessed from pages before
|
||||||
|
obtained.clear();
|
||||||
|
fullSyncAt = System.currentTimeMillis();
|
||||||
|
if (profileKey != null)
|
||||||
|
{
|
||||||
|
configManager.setRSProfileConfiguration(GROUP, KEY_FULL_SYNC, fullSyncAt);
|
||||||
|
}
|
||||||
|
client.addChatMessage(ChatMessageType.GAMEMESSAGE, "", String.format(
|
||||||
|
"<col=ff9040>Essentials Plus:</col> collection log synced - %d / %d items.", have.size(), itemNames.size()), null);
|
||||||
|
}
|
||||||
|
obtained.addAll(have);
|
||||||
|
synced = true;
|
||||||
|
saveObtained();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** When the log was last fully synced through its search, 0 = never. */
|
||||||
|
public long getFullSyncAt()
|
||||||
|
{
|
||||||
|
return fullSyncAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client thread. */
|
||||||
|
public void onChatMessage(ChatMessage event)
|
||||||
|
{
|
||||||
|
if (event.getType() != ChatMessageType.GAMEMESSAGE && event.getType() != ChatMessageType.SPAM)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String msg = Text.removeTags(event.getMessage());
|
||||||
|
if (!msg.startsWith(NEW_ITEM))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String name = msg.substring(NEW_ITEM.length()).trim();
|
||||||
|
boolean changed = false;
|
||||||
|
for (Map.Entry<Integer, String> e : itemNames.entrySet())
|
||||||
|
{
|
||||||
|
if (e.getValue().equalsIgnoreCase(name))
|
||||||
|
{
|
||||||
|
changed |= obtained.add(e.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
|
saveObtained();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset()
|
||||||
|
{
|
||||||
|
profileKey = null;
|
||||||
|
obtained.clear();
|
||||||
|
synced = false;
|
||||||
|
fullSyncAt = 0;
|
||||||
|
searchItems.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True once any collection log data is known for the current profile. */
|
||||||
|
public boolean isSynced()
|
||||||
|
{
|
||||||
|
return synced;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isLoaded()
|
||||||
|
{
|
||||||
|
return !pages.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isObtained(int itemId)
|
||||||
|
{
|
||||||
|
return obtained.contains(itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Tab> getTabs()
|
||||||
|
{
|
||||||
|
return tabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Number of distinct items in the log. */
|
||||||
|
public int totalItems()
|
||||||
|
{
|
||||||
|
return itemNames.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Number of distinct log items the account has obtained. */
|
||||||
|
public int obtainedItems()
|
||||||
|
{
|
||||||
|
int n = 0;
|
||||||
|
for (int id : itemNames.keySet())
|
||||||
|
{
|
||||||
|
if (obtained.contains(id))
|
||||||
|
{
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Obtained items of a page or tab. */
|
||||||
|
public int obtainedCount(List<Integer> itemIds)
|
||||||
|
{
|
||||||
|
int n = 0;
|
||||||
|
for (int id : itemIds)
|
||||||
|
{
|
||||||
|
if (obtained.contains(id))
|
||||||
|
{
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String itemName(int itemId)
|
||||||
|
{
|
||||||
|
return itemNames.getOrDefault(itemId, "Item " + itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** All item ids in the log with the given name (some items appear under several ids). */
|
||||||
|
public Set<Integer> idsByName(String name)
|
||||||
|
{
|
||||||
|
Set<Integer> out = new HashSet<>();
|
||||||
|
for (Map.Entry<Integer, String> e : itemNames.entrySet())
|
||||||
|
{
|
||||||
|
if (e.getValue().equalsIgnoreCase(name))
|
||||||
|
{
|
||||||
|
out.add(e.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The log page of a loot source: exact name, else a page starting with it ("Barrows" -> "Barrows Chests"). */
|
||||||
|
public Page findPage(String source)
|
||||||
|
{
|
||||||
|
if (source == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String s = source.trim().toLowerCase(Locale.ROOT);
|
||||||
|
for (Page p : pages)
|
||||||
|
{
|
||||||
|
if (p.name.toLowerCase(Locale.ROOT).equals(s))
|
||||||
|
{
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Page p : pages)
|
||||||
|
{
|
||||||
|
if (!p.name.equalsIgnoreCase("All Pets") && p.name.toLowerCase(Locale.ROOT).startsWith(s))
|
||||||
|
{
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadPages()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<Page> out = new ArrayList<>();
|
||||||
|
List<Tab> tabList = new ArrayList<>();
|
||||||
|
Map<Integer, String> names = new LinkedHashMap<>();
|
||||||
|
for (int t = 0; t < TAB_STRUCTS.length; t++)
|
||||||
|
{
|
||||||
|
int tabStart = out.size();
|
||||||
|
StructComposition tab = client.getStructComposition(TAB_STRUCTS[t]);
|
||||||
|
EnumComposition pageEnum = client.getEnum(tab.getIntValue(PARAM_TAB_PAGES));
|
||||||
|
for (int pageStructId : pageEnum.getIntVals())
|
||||||
|
{
|
||||||
|
StructComposition page = client.getStructComposition(pageStructId);
|
||||||
|
EnumComposition items = client.getEnum(page.getIntValue(PARAM_PAGE_ITEMS));
|
||||||
|
List<Integer> ids = new ArrayList<>();
|
||||||
|
for (int id : items.getIntVals())
|
||||||
|
{
|
||||||
|
ids.add(id);
|
||||||
|
names.computeIfAbsent(id, i -> client.getItemDefinition(i).getName());
|
||||||
|
}
|
||||||
|
out.add(new Page(page.getStringValue(PARAM_PAGE_NAME), Collections.unmodifiableList(ids)));
|
||||||
|
}
|
||||||
|
tabList.add(new Tab(TAB_NAMES[t], Collections.unmodifiableList(new ArrayList<>(out.subList(tabStart, out.size())))));
|
||||||
|
}
|
||||||
|
itemNames = names;
|
||||||
|
tabs = Collections.unmodifiableList(tabList);
|
||||||
|
pages = Collections.unmodifiableList(out);
|
||||||
|
log.debug("collection log: {} pages, {} items", out.size(), names.size());
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
log.warn("could not read collection log structure", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reads the collection log inventory, which the server sends while the log is open. */
|
||||||
|
private void scanInventory()
|
||||||
|
{
|
||||||
|
ItemContainer inv = client.getItemContainer(CLOG_INVENTORY);
|
||||||
|
if (inv == null || itemNames.isEmpty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Item[] items = inv.getItems();
|
||||||
|
int hash = java.util.Arrays.hashCode(items);
|
||||||
|
if (items.length == 0 || hash == lastInventoryHash)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastInventoryHash = hash;
|
||||||
|
|
||||||
|
Set<Integer> have = new HashSet<>();
|
||||||
|
for (Item it : items)
|
||||||
|
{
|
||||||
|
if (it.getId() > 0 && it.getQuantity() > 0)
|
||||||
|
{
|
||||||
|
have.add(it.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info("collection log inventory: {} slots, {} obtained items", items.length, have.size());
|
||||||
|
EnumComposition remap = client.getEnum(ENUM_ITEM_REMAP);
|
||||||
|
boolean changed = !synced;
|
||||||
|
for (int id : itemNames.keySet())
|
||||||
|
{
|
||||||
|
int mapped = remap.getIntValue(id);
|
||||||
|
// add only: the inventory may hold just the open page, and log items are never lost
|
||||||
|
if (have.contains(id) || (mapped > 0 && have.contains(mapped)))
|
||||||
|
{
|
||||||
|
changed |= obtained.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
synced = true;
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
|
saveObtained();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fallback: items drawn fully opaque on the open log page are obtained. */
|
||||||
|
private void scanOpenPage()
|
||||||
|
{
|
||||||
|
List<Widget> children = new ArrayList<>();
|
||||||
|
for (int component : new int[]{InterfaceID.Collection.ITEMS, InterfaceID.Collection.ITEMS_CONTENTS})
|
||||||
|
{
|
||||||
|
Widget w = client.getWidget(component);
|
||||||
|
if (w == null || w.isHidden() || w.getDynamicChildren() == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (Widget c : w.getDynamicChildren())
|
||||||
|
{
|
||||||
|
if (c != null && c.getItemId() > 0)
|
||||||
|
{
|
||||||
|
children.add(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (children.isEmpty())
|
||||||
|
{
|
||||||
|
lastWidgetHash = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int hash = 1;
|
||||||
|
for (Widget c : children)
|
||||||
|
{
|
||||||
|
hash = 31 * hash + c.getItemId() * 7 + c.getOpacity();
|
||||||
|
}
|
||||||
|
if (hash == lastWidgetHash)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastWidgetHash = hash;
|
||||||
|
log.info("collection log page widgets: {} items", children.size());
|
||||||
|
|
||||||
|
boolean changed = !synced;
|
||||||
|
for (Widget c : children)
|
||||||
|
{
|
||||||
|
int id = c.getItemId();
|
||||||
|
if (c.getOpacity() != UNOBTAINED_OPACITY)
|
||||||
|
{
|
||||||
|
changed |= obtained.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
synced = true;
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
|
saveObtained();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadObtained()
|
||||||
|
{
|
||||||
|
obtained.clear();
|
||||||
|
Long full = configManager.getRSProfileConfiguration(GROUP, KEY_FULL_SYNC, Long.class);
|
||||||
|
fullSyncAt = full == null ? 0 : full;
|
||||||
|
String raw = configManager.getRSProfileConfiguration(GROUP, KEY);
|
||||||
|
synced = raw != null;
|
||||||
|
if (raw == null || raw.isEmpty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (String s : raw.split(","))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
obtained.add(Integer.parseInt(s.trim()));
|
||||||
|
}
|
||||||
|
catch (NumberFormatException ignored)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveObtained()
|
||||||
|
{
|
||||||
|
if (profileKey == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
synced = true;
|
||||||
|
String raw = obtained.stream().sorted().map(String::valueOf).collect(Collectors.joining(","));
|
||||||
|
configManager.setRSProfileConfiguration(GROUP, KEY, raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyListeners()
|
||||||
|
{
|
||||||
|
for (Runnable r : listeners)
|
||||||
|
{
|
||||||
|
r.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package com.example.essentialsplus.currency;
|
||||||
|
|
||||||
|
import net.runelite.api.gameval.ItemID;
|
||||||
|
import net.runelite.api.gameval.VarPlayerID;
|
||||||
|
import net.runelite.api.gameval.VarbitID;
|
||||||
|
|
||||||
|
/** Points, marks and tokens earned from minigames and activities. */
|
||||||
|
public enum Currency
|
||||||
|
{
|
||||||
|
PEST_CONTROL("Pest Control points", Source.WIDGET, -1, ItemID.PEST_VOID_KNIGHT_TOP),
|
||||||
|
SLAYER("Slayer points", Source.VARBIT, VarbitID.SLAYER_POINTS, ItemID.SLAYER_HELM),
|
||||||
|
NMZ("Nightmare Zone points", Source.VARP, VarPlayerID.NZONE_REWARDPOINTS, ItemID.NZONE4DOSEOVERLOADPOTION),
|
||||||
|
TITHE_FARM("Tithe Farm points", Source.VARBIT, VarbitID.HOSIDIUS_TITHE_REWARDPOINTS, ItemID.HOSIDIUS_TITHE_FRUIT_A),
|
||||||
|
TEMPOROSS("Reward permits", Source.VARBIT, VarbitID.TEMPOROSS_REWARDPERMITS, ItemID.SPIRIT_FLAKES),
|
||||||
|
SOUL_WARS("Zeal", Source.VARP, VarPlayerID.SOUL_WARS_ZEAL, ItemID.SOUL_FRAGMENT),
|
||||||
|
GIANTS_FOUNDRY("Foundry reputation", Source.VARP, VarPlayerID.GIANTS_FOUNDRY_REWARD_SHOP_POINTS, ItemID.DOUBLE_AMMO_MOULD),
|
||||||
|
COLOSSEUM("Colosseum glory", Source.VARP, VarPlayerID.COLOSSEUM_GLORY, -1),
|
||||||
|
MIXOLOGY_MOX("Mox resin", Source.VARP, VarPlayerID.MIXOLOGY_MOX_POINTS, ItemID.MM_MOX_PASTE),
|
||||||
|
MIXOLOGY_AGA("Aga resin", Source.VARP, VarPlayerID.MIXOLOGY_AGA_POINTS, ItemID.MM_AGA_PASTE),
|
||||||
|
MIXOLOGY_LYE("Lye resin", Source.VARP, VarPlayerID.MIXOLOGY_LYE_POINTS, ItemID.MM_LYE_PASTE),
|
||||||
|
COMBAT_ACHIEVEMENTS("Combat achievement points", Source.VARBIT, VarbitID.CA_POINTS, -1),
|
||||||
|
BOUNTY_HUNTER("Bounty Hunter points", Source.VARP, VarPlayerID.BH_2023_POINTS, ItemID.BH_EMBLEM),
|
||||||
|
PVP_ARENA("PvP Arena points", Source.VARBIT, VarbitID.PVPA_POINTS_CURRENCY, -1),
|
||||||
|
|
||||||
|
MARKS_OF_GRACE("Marks of grace", Source.ITEM, ItemID.GRACE, ItemID.GRACE),
|
||||||
|
TOKKUL("Tokkul", Source.ITEM, ItemID.TZHAAR_TOKEN, ItemID.TZHAAR_TOKEN),
|
||||||
|
GOLDEN_NUGGETS("Golden nuggets", Source.ITEM, ItemID.MOTHERLODE_NUGGET, ItemID.MOTHERLODE_NUGGET),
|
||||||
|
HALLOWED_MARKS("Hallowed marks", Source.ITEM, ItemID.HALLOWED_MARK, ItemID.HALLOWED_MARK),
|
||||||
|
ABYSSAL_PEARLS("Abyssal pearls", Source.ITEM, ItemID.ABYSSAL_PEARL, ItemID.ABYSSAL_PEARL),
|
||||||
|
STARDUST("Stardust", Source.ITEM, ItemID.STAR_DUST, ItemID.STAR_DUST),
|
||||||
|
MOLCH_PEARLS("Molch pearls", Source.ITEM, ItemID.AERIAL_FISHING_PEARL, ItemID.AERIAL_FISHING_PEARL),
|
||||||
|
NUMULITE("Numulite", Source.ITEM, ItemID.FOSSIL_NUMULITE, ItemID.FOSSIL_NUMULITE),
|
||||||
|
SPIRIT_FLAKES("Spirit flakes", Source.ITEM, ItemID.SPIRIT_FLAKES, ItemID.SPIRIT_FLAKES),
|
||||||
|
ANIMA_BARK("Anima-infused bark", Source.ITEM, ItemID.FORESTRY_CURRENCY, ItemID.FORESTRY_CURRENCY),
|
||||||
|
CRYSTAL_SHARDS("Crystal shards", Source.ITEM, ItemID.PRIF_CRYSTAL_SHARD, ItemID.PRIF_CRYSTAL_SHARD),
|
||||||
|
CASTLE_WARS("Castle wars tickets", Source.ITEM, ItemID.CASTLEWARS_TICKET, ItemID.CASTLEWARS_TICKET),
|
||||||
|
AGILITY_ARENA("Agility arena tickets", Source.ITEM, ItemID.AGILITYARENA_TICKET, ItemID.AGILITYARENA_TICKET),
|
||||||
|
ECTO_TOKENS("Ecto-tokens", Source.ITEM, ItemID.ECTOTOKEN, ItemID.ECTOTOKEN),
|
||||||
|
WARRIOR_GUILD("Warrior guild tokens", Source.ITEM, ItemID.WARGUILD_TOKENS, ItemID.WARGUILD_TOKENS),
|
||||||
|
PIECES_OF_EIGHT("Pieces of eight", Source.ITEM, ItemID.BREW_PIECE_OF_EIGHT, ItemID.BREW_PIECE_OF_EIGHT),
|
||||||
|
MINNOWS("Minnows", Source.ITEM, ItemID.MINNOW, ItemID.MINNOW);
|
||||||
|
|
||||||
|
public enum Source
|
||||||
|
{
|
||||||
|
VARBIT, VARP, ITEM, WIDGET
|
||||||
|
}
|
||||||
|
|
||||||
|
public final String label;
|
||||||
|
public final Source source;
|
||||||
|
/** varbit / varp / item id */
|
||||||
|
public final int id;
|
||||||
|
/** item shown as icon, -1 = letter badge */
|
||||||
|
public final int icon;
|
||||||
|
|
||||||
|
Currency(String label, Source source, int id, int icon)
|
||||||
|
{
|
||||||
|
this.label = label;
|
||||||
|
this.source = source;
|
||||||
|
this.id = id;
|
||||||
|
this.icon = icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Currency byName(String name)
|
||||||
|
{
|
||||||
|
if (name != null)
|
||||||
|
{
|
||||||
|
for (Currency c : values())
|
||||||
|
{
|
||||||
|
if (c.name().equals(name))
|
||||||
|
{
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package com.example.essentialsplus.currency;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.ui.TimeFilter;
|
||||||
|
import com.example.essentialsplus.ui.TimeRange;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/** Point / token amounts of one character over time. */
|
||||||
|
public class CurrencyHistory
|
||||||
|
{
|
||||||
|
/** One observed amount. */
|
||||||
|
public static class Sample
|
||||||
|
{
|
||||||
|
public long t;
|
||||||
|
/** amount held */
|
||||||
|
public long v;
|
||||||
|
/** change counted as earned (> 0) or spent (< 0); 0 for a sync (value changed while not watched) */
|
||||||
|
public long g;
|
||||||
|
|
||||||
|
public Sample()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Sample(long t, long v, long g)
|
||||||
|
{
|
||||||
|
this.t = t;
|
||||||
|
this.v = v;
|
||||||
|
this.g = g;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String key;
|
||||||
|
public String name;
|
||||||
|
/** currency enum name -> samples, oldest first */
|
||||||
|
public Map<String, List<Sample>> series = new HashMap<>();
|
||||||
|
/** currency with the latest earned change and when that happened */
|
||||||
|
public String lastActive;
|
||||||
|
public long lastActiveAt;
|
||||||
|
|
||||||
|
public transient boolean dirty;
|
||||||
|
|
||||||
|
public CurrencyHistory()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public CurrencyHistory(String key, String name)
|
||||||
|
{
|
||||||
|
this.key = key;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
void normalize()
|
||||||
|
{
|
||||||
|
if (series == null)
|
||||||
|
{
|
||||||
|
series = new HashMap<>();
|
||||||
|
}
|
||||||
|
series.values().removeIf(l -> l == null || l.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized List<Sample> samples(Currency c)
|
||||||
|
{
|
||||||
|
List<Sample> s = series.get(c.name());
|
||||||
|
return s == null ? new ArrayList<>() : new ArrayList<>(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean has(Currency c)
|
||||||
|
{
|
||||||
|
List<Sample> s = series.get(c.name());
|
||||||
|
return s != null && !s.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return the latest known amount, -1 if never seen */
|
||||||
|
public synchronized long amount(Currency c)
|
||||||
|
{
|
||||||
|
List<Sample> s = series.get(c.name());
|
||||||
|
return s == null || s.isEmpty() ? -1 : s.get(s.size() - 1).v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Time of the latest earned change, 0 if none. */
|
||||||
|
public synchronized long lastGain(Currency c)
|
||||||
|
{
|
||||||
|
List<Sample> s = series.get(c.name());
|
||||||
|
if (s != null)
|
||||||
|
{
|
||||||
|
for (int i = s.size() - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (s.get(i).g > 0)
|
||||||
|
{
|
||||||
|
return s.get(i).t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores an observed amount.
|
||||||
|
*
|
||||||
|
* @param sync the change happened while it could not be watched (login, bank open): not counted as earned
|
||||||
|
* @return the change counted as earned or spent, 0 when nothing was counted
|
||||||
|
*/
|
||||||
|
synchronized long record(Currency c, long value, long now, boolean sync)
|
||||||
|
{
|
||||||
|
List<Sample> s = series.computeIfAbsent(c.name(), k -> new ArrayList<>());
|
||||||
|
Sample last = s.isEmpty() ? null : s.get(s.size() - 1);
|
||||||
|
if (last != null && last.v == value)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
long delta = last == null || sync ? 0 : value - last.v;
|
||||||
|
s.add(new Sample(now, value, delta));
|
||||||
|
dirty = true;
|
||||||
|
if (delta > 0)
|
||||||
|
{
|
||||||
|
lastActive = c.name();
|
||||||
|
lastActiveAt = now;
|
||||||
|
}
|
||||||
|
return delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Earned (positive) and spent (negative) in the range of the filter. */
|
||||||
|
public synchronized long[] change(Currency c, TimeFilter filter, long now)
|
||||||
|
{
|
||||||
|
List<Sample> s = series.get(c.name());
|
||||||
|
long[] out = new long[2];
|
||||||
|
if (s == null || s.isEmpty())
|
||||||
|
{
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
long start = filter.getRange() == TimeRange.CURRENT
|
||||||
|
? TimeFilter.streakStart(s.size(), i -> s.get(i).t, i -> s.get(i).g > 0, now)
|
||||||
|
: filter.start();
|
||||||
|
for (int i = s.size() - 1; i >= 0 && s.get(i).t >= start; i--)
|
||||||
|
{
|
||||||
|
long g = s.get(i).g;
|
||||||
|
if (g > 0)
|
||||||
|
{
|
||||||
|
out[0] += g;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
out[1] += g;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Earned per bucket over the range, oldest first (for a small bar chart). */
|
||||||
|
public synchronized long[] earnedBuckets(Currency c, long start, long end, int buckets)
|
||||||
|
{
|
||||||
|
long[] out = new long[buckets];
|
||||||
|
List<Sample> s = series.get(c.name());
|
||||||
|
if (s == null || end <= start)
|
||||||
|
{
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
for (Sample x : s)
|
||||||
|
{
|
||||||
|
if (x.g > 0 && x.t >= start && x.t <= end)
|
||||||
|
{
|
||||||
|
int b = (int) Math.min(buckets - 1, (x.t - start) * buckets / (end - start));
|
||||||
|
out[b] += x.g;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package com.example.essentialsplus.currency;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.WealthStore;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.io.Writer;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/** Loads and saves {@link CurrencyHistory} files (one per character) in {@code essentials-plus/currencies}. */
|
||||||
|
@Slf4j
|
||||||
|
@Singleton
|
||||||
|
public class CurrencyStore
|
||||||
|
{
|
||||||
|
public static final File DIR = new File(WealthStore.DIR, "currencies");
|
||||||
|
|
||||||
|
private static final long HOUR = 3_600_000L;
|
||||||
|
private static final long DAY = 24 * HOUR;
|
||||||
|
|
||||||
|
private final Gson gson;
|
||||||
|
private final Map<String, CurrencyHistory> histories = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public CurrencyStore(Gson gson)
|
||||||
|
{
|
||||||
|
this.gson = gson.newBuilder().disableHtmlEscaping().create();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void load()
|
||||||
|
{
|
||||||
|
File[] files = DIR.listFiles((d, n) -> n.endsWith(".json"));
|
||||||
|
if (files == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (File f : files)
|
||||||
|
{
|
||||||
|
try (Reader reader = Files.newBufferedReader(f.toPath(), StandardCharsets.UTF_8))
|
||||||
|
{
|
||||||
|
CurrencyHistory h = gson.fromJson(reader, CurrencyHistory.class);
|
||||||
|
if (h != null && h.key != null)
|
||||||
|
{
|
||||||
|
h.normalize();
|
||||||
|
histories.putIfAbsent(h.key, h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
log.warn("Currencies: could not read {}", f, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public CurrencyHistory get(String key)
|
||||||
|
{
|
||||||
|
return key == null ? null : histories.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrencyHistory getOrCreate(String key, String name)
|
||||||
|
{
|
||||||
|
CurrencyHistory h = histories.computeIfAbsent(key, k -> new CurrencyHistory(k, name));
|
||||||
|
h.name = name;
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveDirty()
|
||||||
|
{
|
||||||
|
for (CurrencyHistory h : histories.values())
|
||||||
|
{
|
||||||
|
if (h.dirty)
|
||||||
|
{
|
||||||
|
save(h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void save(CurrencyHistory h)
|
||||||
|
{
|
||||||
|
String json;
|
||||||
|
synchronized (h)
|
||||||
|
{
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
for (Map.Entry<String, List<CurrencyHistory.Sample>> e : h.series.entrySet())
|
||||||
|
{
|
||||||
|
e.setValue(compact(e.getValue(), now));
|
||||||
|
}
|
||||||
|
json = gson.toJson(h);
|
||||||
|
h.dirty = false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Files.createDirectories(DIR.toPath());
|
||||||
|
File target = new File(DIR, h.key.replaceAll("[^A-Za-z0-9 _()\\-]", "_") + ".json");
|
||||||
|
File tmp = new File(DIR, target.getName() + ".tmp");
|
||||||
|
try (Writer w = Files.newBufferedWriter(tmp.toPath(), StandardCharsets.UTF_8))
|
||||||
|
{
|
||||||
|
w.write(json);
|
||||||
|
}
|
||||||
|
Files.move(tmp.toPath(), target.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
h.dirty = true;
|
||||||
|
log.warn("Currencies: could not save {}", h.key, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Samples older than 7 days are merged per hour, older than 90 days per day: the last amount is kept
|
||||||
|
* and earned / spent are summed separately. The first sample stays as baseline.
|
||||||
|
*/
|
||||||
|
static List<CurrencyHistory.Sample> compact(List<CurrencyHistory.Sample> in, long now)
|
||||||
|
{
|
||||||
|
if (in.size() < 3)
|
||||||
|
{
|
||||||
|
return in;
|
||||||
|
}
|
||||||
|
List<CurrencyHistory.Sample> out = new ArrayList<>(in.size());
|
||||||
|
out.add(in.get(0));
|
||||||
|
for (int i = 1; i < in.size(); i++)
|
||||||
|
{
|
||||||
|
CurrencyHistory.Sample s = in.get(i);
|
||||||
|
long age = now - s.t;
|
||||||
|
long size = age > 90 * DAY ? DAY : age > 7 * DAY ? HOUR : 0;
|
||||||
|
CurrencyHistory.Sample prev = out.get(out.size() - 1);
|
||||||
|
// only merge changes of the same sign, so earned and spent stay apart
|
||||||
|
if (size > 0 && out.size() > 1 && prev.t / size == s.t / size && Long.signum(prev.g) * Long.signum(s.g) >= 0)
|
||||||
|
{
|
||||||
|
out.set(out.size() - 1, new CurrencyHistory.Sample(s.t, s.v, prev.g + s.g));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
out.add(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
package com.example.essentialsplus.currency;
|
||||||
|
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.function.IntUnaryOperator;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.gameval.InterfaceID;
|
||||||
|
import net.runelite.api.widgets.Widget;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads point varbits / varps every tick and token items after each wealth valuation, records the changes and
|
||||||
|
* works out what the player is doing right now. Client thread only, except the getters.
|
||||||
|
*/
|
||||||
|
public class CurrencyTracker
|
||||||
|
{
|
||||||
|
/** a currency stays "what you are doing" this long after it was last earned */
|
||||||
|
public static final long ACTIVE_WINDOW = 20 * 60_000L;
|
||||||
|
/** varps read 0 or stale values right after login: changes until then are synced, not earned */
|
||||||
|
private static final int SETTLE_TICKS = 10;
|
||||||
|
private static final String[] ITEM_CONTAINERS = {"bank", "inventory", "equipment", "lootingBag", "seedVault"};
|
||||||
|
private static final Pattern NUMBER = Pattern.compile("(\\d[\\d,]*)");
|
||||||
|
private static final Map<Integer, Currency> ITEMS = new HashMap<>();
|
||||||
|
|
||||||
|
static
|
||||||
|
{
|
||||||
|
for (Currency c : Currency.values())
|
||||||
|
{
|
||||||
|
if (c.source == Currency.Source.ITEM)
|
||||||
|
{
|
||||||
|
ITEMS.put(c.id, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Client client;
|
||||||
|
private final CurrencyStore store;
|
||||||
|
private final List<Runnable> listeners = new CopyOnWriteArrayList<>();
|
||||||
|
/** currencies read at least once since login; the first reading is a sync */
|
||||||
|
private final Set<Currency> seen = EnumSet.noneOf(Currency.class);
|
||||||
|
|
||||||
|
private volatile String key;
|
||||||
|
/** Pest Control lander / game overlay is on screen */
|
||||||
|
private volatile boolean inPestControl;
|
||||||
|
|
||||||
|
public CurrencyTracker(Client client, CurrencyStore store)
|
||||||
|
{
|
||||||
|
this.client = client;
|
||||||
|
this.store = store;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addListener(Runnable r)
|
||||||
|
{
|
||||||
|
listeners.add(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeListener(Runnable r)
|
||||||
|
{
|
||||||
|
listeners.remove(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKey()
|
||||||
|
{
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CurrencyHistory history()
|
||||||
|
{
|
||||||
|
return store.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Logged out: the next login starts with a sync again. */
|
||||||
|
public void reset()
|
||||||
|
{
|
||||||
|
seen.clear();
|
||||||
|
key = null;
|
||||||
|
inPestControl = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onGameTick(String accountKey, String name, int ticksSinceLogin)
|
||||||
|
{
|
||||||
|
if (accountKey == null || ticksSinceLogin < 3)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!accountKey.equals(key))
|
||||||
|
{
|
||||||
|
seen.clear();
|
||||||
|
key = accountKey;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
CurrencyHistory h = store.getOrCreate(accountKey, name);
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
boolean settled = ticksSinceLogin >= SETTLE_TICKS;
|
||||||
|
boolean changed = false;
|
||||||
|
for (Currency c : Currency.values())
|
||||||
|
{
|
||||||
|
long value;
|
||||||
|
switch (c.source)
|
||||||
|
{
|
||||||
|
case VARBIT:
|
||||||
|
value = client.getVarbitValue(c.id);
|
||||||
|
break;
|
||||||
|
case VARP:
|
||||||
|
value = client.getVarpValue(c.id);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
changed |= record(h, c, value, now, !settled);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget lander = visible(InterfaceID.PestLanderOverlay.PEST_LANDER_OVER_POINTS);
|
||||||
|
boolean pest = lander != null || visible(InterfaceID.PestStatusOverlay.PEST_STATUS_OVER_TIME) != null;
|
||||||
|
if (pest != inPestControl)
|
||||||
|
{
|
||||||
|
inPestControl = pest;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
long points = parse(lander);
|
||||||
|
if (points < 0)
|
||||||
|
{
|
||||||
|
points = parse(visible(InterfaceID.PestRewardshop.TOTAL));
|
||||||
|
}
|
||||||
|
if (points >= 0)
|
||||||
|
{
|
||||||
|
changed |= record(h, Currency.PEST_CONTROL, points, now, false);
|
||||||
|
}
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts token items over all known containers of the character.
|
||||||
|
*
|
||||||
|
* @param sync items moved in a way that is not earning or spending (bank open, login)
|
||||||
|
*/
|
||||||
|
public void onItems(String accountKey, String name, Map<String, Map<Integer, Long>> containers, Map<Integer, Long> bankPending,
|
||||||
|
boolean sync, IntUnaryOperator canonicalize)
|
||||||
|
{
|
||||||
|
if (accountKey == null || containers == null || !accountKey.equals(key))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<Currency, Long> totals = new HashMap<>();
|
||||||
|
for (String k : ITEM_CONTAINERS)
|
||||||
|
{
|
||||||
|
count(containers.get(k), canonicalize, totals);
|
||||||
|
}
|
||||||
|
count(bankPending, canonicalize, totals);
|
||||||
|
CurrencyHistory h = store.getOrCreate(accountKey, name);
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
boolean changed = false;
|
||||||
|
for (Currency c : ITEMS.values())
|
||||||
|
{
|
||||||
|
long value = totals.getOrDefault(c, 0L);
|
||||||
|
// never seen and none held: nothing to show
|
||||||
|
if (value == 0 && !h.has(c))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
changed |= record(h, c, value, now, sync);
|
||||||
|
}
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void count(Map<Integer, Long> items, IntUnaryOperator canonicalize, Map<Currency, Long> totals)
|
||||||
|
{
|
||||||
|
if (items == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (Map.Entry<Integer, Long> e : items.entrySet())
|
||||||
|
{
|
||||||
|
Currency c = ITEMS.get(e.getKey());
|
||||||
|
if (c == null)
|
||||||
|
{
|
||||||
|
c = ITEMS.get(canonicalize.applyAsInt(e.getKey()));
|
||||||
|
}
|
||||||
|
if (c != null)
|
||||||
|
{
|
||||||
|
totals.merge(c, e.getValue(), Long::sum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean record(CurrencyHistory h, Currency c, long value, long now, boolean sync)
|
||||||
|
{
|
||||||
|
if (value < 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// a currency never held is not worth a row
|
||||||
|
if (value == 0 && !h.has(c))
|
||||||
|
{
|
||||||
|
seen.add(c);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
boolean first = seen.add(c);
|
||||||
|
long before = h.amount(c);
|
||||||
|
h.record(c, value, now, sync || first);
|
||||||
|
return before != value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What the player is doing right now: Pest Control while its overlay is shown, otherwise the currency
|
||||||
|
* earned last within {@link #ACTIVE_WINDOW}; null when idle.
|
||||||
|
*/
|
||||||
|
public Currency active()
|
||||||
|
{
|
||||||
|
if (inPestControl)
|
||||||
|
{
|
||||||
|
return Currency.PEST_CONTROL;
|
||||||
|
}
|
||||||
|
CurrencyHistory h = history();
|
||||||
|
if (h == null || h.lastActive == null || System.currentTimeMillis() - h.lastActiveAt > ACTIVE_WINDOW)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Currency.byName(h.lastActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Widget visible(int component)
|
||||||
|
{
|
||||||
|
Widget w = client.getWidget(component);
|
||||||
|
return w == null || w.isHidden() ? null : w;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long parse(Widget w)
|
||||||
|
{
|
||||||
|
if (w == null || w.getText() == null)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
Matcher m = NUMBER.matcher(w.getText().replaceAll("<[^>]*>", ""));
|
||||||
|
long last = -1;
|
||||||
|
while (m.find())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
last = Long.parseLong(m.group(1).replace(",", ""));
|
||||||
|
}
|
||||||
|
catch (NumberFormatException ignored)
|
||||||
|
{
|
||||||
|
// keep the previous number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return last;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyListeners()
|
||||||
|
{
|
||||||
|
for (Runnable r : listeners)
|
||||||
|
{
|
||||||
|
r.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
package com.example.essentialsplus.currency.ui;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.currency.Currency;
|
||||||
|
import com.example.essentialsplus.ui.Ui;
|
||||||
|
import java.awt.Cursor;
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.Font;
|
||||||
|
import java.awt.FontMetrics;
|
||||||
|
import java.awt.Graphics;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.event.MouseAdapter;
|
||||||
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.text.NumberFormat;
|
||||||
|
import java.util.Locale;
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
import javax.swing.SwingUtilities;
|
||||||
|
import net.runelite.client.ui.FontManager;
|
||||||
|
import net.runelite.client.ui.PluginPanel;
|
||||||
|
|
||||||
|
/** Overview entry: the points / marks of what is being played right now, big. */
|
||||||
|
public class CurrencyCard extends JComponent
|
||||||
|
{
|
||||||
|
private static final int ICON = 36;
|
||||||
|
|
||||||
|
private final CurrencyIcons icons;
|
||||||
|
private Currency currency;
|
||||||
|
private long amount;
|
||||||
|
private long earned;
|
||||||
|
private String rangeLabel = "";
|
||||||
|
private boolean now;
|
||||||
|
private long lastAt;
|
||||||
|
private int tracked;
|
||||||
|
private boolean hover;
|
||||||
|
private boolean showTracked = true;
|
||||||
|
private boolean pinned;
|
||||||
|
|
||||||
|
CurrencyCard(CurrencyIcons icons, Runnable onClick)
|
||||||
|
{
|
||||||
|
this.icons = icons;
|
||||||
|
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
||||||
|
addMouseListener(new MouseAdapter()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void mouseEntered(MouseEvent e)
|
||||||
|
{
|
||||||
|
hover = true;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseExited(MouseEvent e)
|
||||||
|
{
|
||||||
|
hover = false;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mousePressed(MouseEvent e)
|
||||||
|
{
|
||||||
|
if (SwingUtilities.isLeftMouseButton(e))
|
||||||
|
{
|
||||||
|
onClick.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param currency shown currency, null when nothing is tracked yet
|
||||||
|
* @param now it is being played right now (otherwise it is the one played last)
|
||||||
|
*/
|
||||||
|
void update(Currency currency, long amount, long earned, String rangeLabel, boolean now, long lastAt, int tracked)
|
||||||
|
{
|
||||||
|
this.currency = currency;
|
||||||
|
this.amount = amount;
|
||||||
|
this.earned = earned;
|
||||||
|
this.rangeLabel = rangeLabel;
|
||||||
|
this.now = now;
|
||||||
|
this.lastAt = lastAt;
|
||||||
|
this.tracked = tracked;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setOptions(boolean showTracked, boolean pinned)
|
||||||
|
{
|
||||||
|
this.showTracked = showTracked;
|
||||||
|
this.pinned = pinned;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
static String number(long v)
|
||||||
|
{
|
||||||
|
return v >= 10_000_000 ? Ui.gp(v) : NumberFormat.getIntegerInstance(Locale.US).format(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String ago(long t)
|
||||||
|
{
|
||||||
|
long d = System.currentTimeMillis() - t;
|
||||||
|
if (t <= 0)
|
||||||
|
{
|
||||||
|
return "never";
|
||||||
|
}
|
||||||
|
if (d < Ui.MINUTE)
|
||||||
|
{
|
||||||
|
return "just now";
|
||||||
|
}
|
||||||
|
if (d < Ui.HOUR)
|
||||||
|
{
|
||||||
|
return d / Ui.MINUTE + "m ago";
|
||||||
|
}
|
||||||
|
if (d < Ui.DAY)
|
||||||
|
{
|
||||||
|
return d / Ui.HOUR + "h ago";
|
||||||
|
}
|
||||||
|
return d / Ui.DAY + "d ago";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension getPreferredSize()
|
||||||
|
{
|
||||||
|
return new Dimension(PluginPanel.PANEL_WIDTH - 16, 104);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension getMinimumSize()
|
||||||
|
{
|
||||||
|
return new Dimension(10, 96);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void paintComponent(Graphics g0)
|
||||||
|
{
|
||||||
|
Graphics2D g = Ui.prepare(g0);
|
||||||
|
int w = getWidth();
|
||||||
|
int h = getHeight();
|
||||||
|
g.setColor(hover ? Ui.CARD_HOVER : Ui.CARD);
|
||||||
|
g.fillRoundRect(0, 0, w, h, 8, 8);
|
||||||
|
|
||||||
|
int pad = 10;
|
||||||
|
Font bold = FontManager.getRunescapeBoldFont();
|
||||||
|
Font small = FontManager.getRunescapeSmallFont();
|
||||||
|
|
||||||
|
g.setFont(bold);
|
||||||
|
FontMetrics fm = g.getFontMetrics();
|
||||||
|
int head = 6 + fm.getAscent();
|
||||||
|
g.setColor(Ui.ACCENT);
|
||||||
|
String title = "Points & Marks";
|
||||||
|
g.drawString(title, pad, head);
|
||||||
|
g.setFont(bold.deriveFont(14f));
|
||||||
|
g.setColor(hover ? Ui.ACCENT : Ui.FAINT);
|
||||||
|
Ui.drawRight(g, "›", w - pad, head);
|
||||||
|
|
||||||
|
g.setFont(small);
|
||||||
|
FontMetrics sfm = g.getFontMetrics();
|
||||||
|
int subRight = w - pad - 14;
|
||||||
|
if (currency != null)
|
||||||
|
{
|
||||||
|
String sub = now ? "● Playing now" : pinned ? "Pinned" : "Last: " + ago(lastAt);
|
||||||
|
g.setColor(now ? Ui.UP : Ui.MUTED);
|
||||||
|
if (subRight - sfm.stringWidth(sub) > pad + fm.stringWidth(title) + 8)
|
||||||
|
{
|
||||||
|
Ui.drawRight(g, sub, subRight, head);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currency == null)
|
||||||
|
{
|
||||||
|
g.setFont(FontManager.getRunescapeFont());
|
||||||
|
g.setColor(Ui.TEXT);
|
||||||
|
g.drawString("Nothing tracked yet", pad, head + 22);
|
||||||
|
g.setFont(small);
|
||||||
|
g.setColor(Ui.FAINT);
|
||||||
|
g.drawString("Play a minigame or collect marks,", pad, head + 40);
|
||||||
|
g.drawString("tokens or points to start", pad, head + 54);
|
||||||
|
g.dispose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int top = head + 8;
|
||||||
|
icons.paint(g, currency, pad - 2, top + 4, ICON, this);
|
||||||
|
int tx = pad + ICON + 8;
|
||||||
|
|
||||||
|
g.setFont(bold.deriveFont(26f));
|
||||||
|
FontMetrics bfm = g.getFontMetrics();
|
||||||
|
int bigBase = top + bfm.getAscent() - 2;
|
||||||
|
g.setColor(Ui.GOLD);
|
||||||
|
g.drawString(number(amount), tx, bigBase);
|
||||||
|
|
||||||
|
g.setFont(small);
|
||||||
|
int labelBase = bigBase + sfm.getAscent() + 3;
|
||||||
|
g.setColor(Ui.TEXT);
|
||||||
|
g.drawString(currency.label, tx, labelBase);
|
||||||
|
|
||||||
|
int earnBase = labelBase + sfm.getAscent() + 6;
|
||||||
|
String gain = "+" + number(earned);
|
||||||
|
g.setFont(bold);
|
||||||
|
FontMetrics gfm = g.getFontMetrics();
|
||||||
|
g.setColor(earned > 0 ? Ui.UP : Ui.FAINT);
|
||||||
|
g.drawString(gain, tx, earnBase);
|
||||||
|
g.setFont(small);
|
||||||
|
g.setColor(Ui.FAINT);
|
||||||
|
int afterGain = tx + gfm.stringWidth(gain);
|
||||||
|
String earnedText = Ui.ellipsize(sfm, " earned " + rangeLabel, w - pad - afterGain);
|
||||||
|
g.drawString(earnedText, afterGain, earnBase);
|
||||||
|
if (showTracked && tracked > 1)
|
||||||
|
{
|
||||||
|
// in the label line, right of the name - never over the earned text
|
||||||
|
String t = tracked + " tracked";
|
||||||
|
if (w - pad - sfm.stringWidth(t) > tx + sfm.stringWidth(currency.label) + 8)
|
||||||
|
{
|
||||||
|
Ui.drawRight(g, t, w - pad, labelBase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ui.paintDragOutline(g, this);
|
||||||
|
g.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.example.essentialsplus.currency.ui;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.currency.Currency;
|
||||||
|
import com.example.essentialsplus.ui.Ui;
|
||||||
|
import java.awt.Component;
|
||||||
|
import java.awt.Font;
|
||||||
|
import java.awt.FontMetrics;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import net.runelite.client.game.ItemManager;
|
||||||
|
import net.runelite.client.ui.FontManager;
|
||||||
|
import net.runelite.client.util.AsyncBufferedImage;
|
||||||
|
|
||||||
|
/** Item icons of the currencies, with a letter badge for those without one (Swing thread only). */
|
||||||
|
final class CurrencyIcons
|
||||||
|
{
|
||||||
|
private final ItemManager itemManager;
|
||||||
|
private final Map<Currency, AsyncBufferedImage> icons = new EnumMap<>(Currency.class);
|
||||||
|
|
||||||
|
CurrencyIcons(ItemManager itemManager)
|
||||||
|
{
|
||||||
|
this.itemManager = itemManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
void paint(Graphics2D g, Currency c, int x, int y, int size, Component repaintTarget)
|
||||||
|
{
|
||||||
|
if (c.icon > 0)
|
||||||
|
{
|
||||||
|
AsyncBufferedImage img = icons.get(c);
|
||||||
|
if (img == null)
|
||||||
|
{
|
||||||
|
img = itemManager.getImage(c.icon);
|
||||||
|
icons.put(c, img);
|
||||||
|
img.onLoaded(repaintTarget::repaint);
|
||||||
|
}
|
||||||
|
int w = img.getWidth();
|
||||||
|
int h = img.getHeight();
|
||||||
|
if (w > 0 && h > 0)
|
||||||
|
{
|
||||||
|
double scale = Math.min(1.0, Math.min((double) size / w, (double) size / h));
|
||||||
|
int dw = (int) Math.round(w * scale);
|
||||||
|
int dh = (int) Math.round(h * scale);
|
||||||
|
g.drawImage(img, x + (size - dw) / 2, y + (size - dh) / 2, dw, dh, null);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g.setColor(Ui.alpha(Ui.ACCENT, 50));
|
||||||
|
g.fillOval(x + 2, y + 2, size - 4, size - 4);
|
||||||
|
Font f = FontManager.getRunescapeBoldFont();
|
||||||
|
g.setFont(f);
|
||||||
|
FontMetrics fm = g.getFontMetrics();
|
||||||
|
String s = c.label.substring(0, 1);
|
||||||
|
g.setColor(Ui.ACCENT);
|
||||||
|
g.drawString(s, x + (size - fm.stringWidth(s)) / 2, y + (size + fm.getAscent()) / 2 - 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,353 @@
|
|||||||
|
package com.example.essentialsplus.currency.ui;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.currency.Currency;
|
||||||
|
import com.example.essentialsplus.currency.CurrencyHistory;
|
||||||
|
import com.example.essentialsplus.currency.CurrencyTracker;
|
||||||
|
import com.example.essentialsplus.ui.TimeFilter;
|
||||||
|
import com.example.essentialsplus.ui.Ui;
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.Cursor;
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.Font;
|
||||||
|
import java.awt.FontMetrics;
|
||||||
|
import java.awt.Graphics;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.swing.BorderFactory;
|
||||||
|
import javax.swing.JButton;
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JScrollPane;
|
||||||
|
import javax.swing.ScrollPaneConstants;
|
||||||
|
import javax.swing.SwingUtilities;
|
||||||
|
import net.runelite.client.game.ItemManager;
|
||||||
|
import net.runelite.client.ui.DynamicGridLayout;
|
||||||
|
import net.runelite.client.ui.FontManager;
|
||||||
|
import net.runelite.client.ui.PluginPanel;
|
||||||
|
|
||||||
|
/** Points & Marks tab: every tracked minigame point, mark and token with the amount earned in the range. */
|
||||||
|
public class CurrencyPanel extends JPanel
|
||||||
|
{
|
||||||
|
private final CurrencyTracker tracker;
|
||||||
|
private final TimeFilter filter;
|
||||||
|
private final CurrencyIcons icons;
|
||||||
|
private final JPanel list = new JPanel(new DynamicGridLayout(0, 1, 0, 4));
|
||||||
|
private final CurrencyCard hero;
|
||||||
|
private CurrencyCard overviewCard;
|
||||||
|
/** currency always shown on the overview card, null = what is being played */
|
||||||
|
private Currency overviewPin;
|
||||||
|
private boolean overviewShowTracked = true;
|
||||||
|
|
||||||
|
public CurrencyPanel(CurrencyTracker tracker, ItemManager itemManager, TimeFilter filter, Runnable onBack)
|
||||||
|
{
|
||||||
|
this.tracker = tracker;
|
||||||
|
this.filter = filter;
|
||||||
|
this.icons = new CurrencyIcons(itemManager);
|
||||||
|
this.hero = new CurrencyCard(icons, () -> { });
|
||||||
|
hero.setCursor(Cursor.getDefaultCursor());
|
||||||
|
|
||||||
|
setLayout(new BorderLayout(0, 6));
|
||||||
|
setBackground(Ui.BG);
|
||||||
|
setBorder(BorderFactory.createEmptyBorder(10, 8, 8, 8));
|
||||||
|
|
||||||
|
JPanel backRow = new JPanel(new BorderLayout(6, 0));
|
||||||
|
backRow.setOpaque(false);
|
||||||
|
JButton backBtn = new JButton("← Overview");
|
||||||
|
backBtn.setFont(FontManager.getRunescapeSmallFont());
|
||||||
|
backBtn.setForeground(Ui.ACCENT);
|
||||||
|
backBtn.setFocusPainted(false);
|
||||||
|
backBtn.setMargin(new java.awt.Insets(2, 2, 2, 2));
|
||||||
|
backBtn.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
||||||
|
backBtn.addActionListener(e -> onBack.run());
|
||||||
|
backRow.add(backBtn, BorderLayout.WEST);
|
||||||
|
JLabel title = new JLabel("Points & Marks");
|
||||||
|
title.setFont(FontManager.getRunescapeBoldFont());
|
||||||
|
title.setForeground(Ui.ACCENT);
|
||||||
|
title.setHorizontalAlignment(JLabel.RIGHT);
|
||||||
|
backRow.add(title, BorderLayout.CENTER);
|
||||||
|
add(backRow, BorderLayout.NORTH);
|
||||||
|
|
||||||
|
list.setOpaque(false);
|
||||||
|
JPanel listWrap = new JPanel(new BorderLayout());
|
||||||
|
listWrap.setOpaque(false);
|
||||||
|
listWrap.add(list, BorderLayout.NORTH);
|
||||||
|
JScrollPane scroll = new JScrollPane(listWrap);
|
||||||
|
scroll.setBorder(null);
|
||||||
|
scroll.setOpaque(false);
|
||||||
|
scroll.getViewport().setOpaque(false);
|
||||||
|
scroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
|
||||||
|
scroll.getVerticalScrollBar().setUnitIncrement(16);
|
||||||
|
scroll.getVerticalScrollBar().setPreferredSize(new Dimension(8, 0));
|
||||||
|
add(scroll, BorderLayout.CENTER);
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOverviewOptions(Currency pin, boolean showTracked)
|
||||||
|
{
|
||||||
|
overviewPin = pin;
|
||||||
|
overviewShowTracked = showTracked;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Currencies with data for the logged-in character, in enum order. */
|
||||||
|
public List<Currency> tracked()
|
||||||
|
{
|
||||||
|
List<Currency> out = new ArrayList<>();
|
||||||
|
CurrencyHistory h = tracker.history();
|
||||||
|
if (h != null)
|
||||||
|
{
|
||||||
|
for (Currency c : Currency.values())
|
||||||
|
{
|
||||||
|
if (h.has(c))
|
||||||
|
{
|
||||||
|
out.add(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Card for the overview, kept up to date by {@link #refresh}. */
|
||||||
|
public CurrencyCard createOverviewCard(Runnable onClick)
|
||||||
|
{
|
||||||
|
overviewCard = new CurrencyCard(icons, onClick);
|
||||||
|
refresh();
|
||||||
|
return overviewCard;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Swing thread. */
|
||||||
|
public void refresh()
|
||||||
|
{
|
||||||
|
CurrencyHistory h = tracker.history();
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
String range = filter.inLabel();
|
||||||
|
|
||||||
|
List<Currency> shown = new ArrayList<>();
|
||||||
|
if (h != null)
|
||||||
|
{
|
||||||
|
for (Currency c : Currency.values())
|
||||||
|
{
|
||||||
|
if (h.has(c))
|
||||||
|
{
|
||||||
|
shown.add(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Currency active = tracker.active();
|
||||||
|
Currency featured = active;
|
||||||
|
if (featured == null && h != null)
|
||||||
|
{
|
||||||
|
featured = Currency.byName(h.lastActive);
|
||||||
|
if (featured == null)
|
||||||
|
{
|
||||||
|
long best = -1;
|
||||||
|
for (Currency c : shown)
|
||||||
|
{
|
||||||
|
List<CurrencyHistory.Sample> s = h.samples(c);
|
||||||
|
long t = s.get(s.size() - 1).t;
|
||||||
|
if (t > best)
|
||||||
|
{
|
||||||
|
best = t;
|
||||||
|
featured = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (CurrencyCard card : new CurrencyCard[]{hero, overviewCard})
|
||||||
|
{
|
||||||
|
if (card == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Currency c = featured;
|
||||||
|
boolean pinned = card == overviewCard && overviewPin != null && h != null && h.has(overviewPin);
|
||||||
|
if (pinned)
|
||||||
|
{
|
||||||
|
c = overviewPin;
|
||||||
|
}
|
||||||
|
card.setOptions(card != overviewCard || overviewShowTracked, pinned && c != active);
|
||||||
|
if (c == null || h == null)
|
||||||
|
{
|
||||||
|
card.update(null, 0, 0, range, false, 0, 0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
card.update(c, Math.max(0, h.amount(c)), h.change(c, filter, now)[0], range,
|
||||||
|
c == active, h.lastGain(c), shown.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
list.removeAll();
|
||||||
|
list.add(hero);
|
||||||
|
if (h == null || shown.isEmpty())
|
||||||
|
{
|
||||||
|
list.add(hint(h == null
|
||||||
|
? "Log in to start tracking."
|
||||||
|
: "Nothing tracked yet. Points show up once they change or are read in-game."));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// earned in the range first, then most recently earned
|
||||||
|
shown.sort((a, b) ->
|
||||||
|
{
|
||||||
|
int byActive = Boolean.compare(b == active, a == active);
|
||||||
|
if (byActive != 0)
|
||||||
|
{
|
||||||
|
return byActive;
|
||||||
|
}
|
||||||
|
return Long.compare(h.lastGain(b), h.lastGain(a));
|
||||||
|
});
|
||||||
|
addSection("POINTS", shown, h, active, now, false);
|
||||||
|
addSection("MARKS & TOKENS", shown, h, active, now, true);
|
||||||
|
}
|
||||||
|
list.add(hint("Pest Control points are read while the lander or reward shop is open. "
|
||||||
|
+ "Items count bank, inventory, gear, looting bag and seed vault."));
|
||||||
|
list.revalidate();
|
||||||
|
list.repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addSection(String name, List<Currency> shown, CurrencyHistory h, Currency active, long now, boolean items)
|
||||||
|
{
|
||||||
|
boolean header = false;
|
||||||
|
for (Currency c : shown)
|
||||||
|
{
|
||||||
|
if ((c.source == Currency.Source.ITEM) != items)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!header)
|
||||||
|
{
|
||||||
|
list.add(header(name));
|
||||||
|
header = true;
|
||||||
|
}
|
||||||
|
long[] change = h.change(c, filter, now);
|
||||||
|
list.add(new Row(c, h.amount(c), change[0], change[1], h.lastGain(c), c == active));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JLabel header(String text)
|
||||||
|
{
|
||||||
|
JLabel l = new JLabel(text);
|
||||||
|
l.setFont(FontManager.getRunescapeSmallFont());
|
||||||
|
l.setForeground(Ui.MUTED);
|
||||||
|
l.setBorder(BorderFactory.createEmptyBorder(6, 2, 0, 2));
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JLabel hint(String text)
|
||||||
|
{
|
||||||
|
JLabel l = new JLabel("<html>" + text + "</html>");
|
||||||
|
l.setFont(FontManager.getRunescapeSmallFont());
|
||||||
|
l.setForeground(Ui.FAINT);
|
||||||
|
l.setBorder(BorderFactory.createEmptyBorder(6, 4, 6, 4));
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One currency: icon, name, amount, earned / spent in the range. */
|
||||||
|
private final class Row extends JComponent
|
||||||
|
{
|
||||||
|
private static final int ICON = 28;
|
||||||
|
private final Currency currency;
|
||||||
|
private final long amount;
|
||||||
|
private final long earned;
|
||||||
|
private final long spent;
|
||||||
|
private final long lastGain;
|
||||||
|
private final boolean active;
|
||||||
|
|
||||||
|
Row(Currency currency, long amount, long earned, long spent, long lastGain, boolean active)
|
||||||
|
{
|
||||||
|
this.currency = currency;
|
||||||
|
this.amount = amount;
|
||||||
|
this.earned = earned;
|
||||||
|
this.spent = spent;
|
||||||
|
this.lastGain = lastGain;
|
||||||
|
this.active = active;
|
||||||
|
setToolTipText(currency.label + ": " + CurrencyCard.number(amount)
|
||||||
|
+ (lastGain > 0 ? " - last earned " + CurrencyCard.ago(lastGain) : ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension getPreferredSize()
|
||||||
|
{
|
||||||
|
return new Dimension(PluginPanel.PANEL_WIDTH - 24, 44);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void paintComponent(Graphics g0)
|
||||||
|
{
|
||||||
|
Graphics2D g = Ui.prepare(g0);
|
||||||
|
int w = getWidth();
|
||||||
|
int h = getHeight();
|
||||||
|
g.setColor(active ? Ui.CARD_SELECTED : Ui.CARD);
|
||||||
|
g.fillRoundRect(0, 0, w, h, 6, 6);
|
||||||
|
if (active)
|
||||||
|
{
|
||||||
|
g.setColor(Ui.UP);
|
||||||
|
g.fillRoundRect(0, 6, 3, h - 12, 3, 3);
|
||||||
|
}
|
||||||
|
int pad = 8;
|
||||||
|
icons.paint(g, currency, pad - 2, (h - ICON) / 2, ICON, this);
|
||||||
|
int tx = pad + ICON + 6;
|
||||||
|
|
||||||
|
Font bold = FontManager.getRunescapeBoldFont();
|
||||||
|
Font small = FontManager.getRunescapeSmallFont();
|
||||||
|
g.setFont(bold);
|
||||||
|
FontMetrics bfm = g.getFontMetrics();
|
||||||
|
String value = CurrencyCard.number(Math.max(0, amount));
|
||||||
|
int line1 = 5 + bfm.getAscent();
|
||||||
|
g.setColor(Ui.GOLD);
|
||||||
|
Ui.drawRight(g, value, w - pad, line1);
|
||||||
|
|
||||||
|
g.setFont(FontManager.getRunescapeFont());
|
||||||
|
FontMetrics nfm = g.getFontMetrics();
|
||||||
|
g.setColor(Ui.TEXT);
|
||||||
|
int nameMax = w - pad - bfm.stringWidth(value) - 8 - tx;
|
||||||
|
g.drawString(clip(nfm, currency.label, nameMax), tx, line1);
|
||||||
|
|
||||||
|
g.setFont(small);
|
||||||
|
FontMetrics sfm = g.getFontMetrics();
|
||||||
|
int line2 = line1 + sfm.getAscent() + 6;
|
||||||
|
String plus = "+" + CurrencyCard.number(earned);
|
||||||
|
g.setColor(earned > 0 ? Ui.UP : Ui.FAINT);
|
||||||
|
g.drawString(plus, tx, line2);
|
||||||
|
int x = tx + sfm.stringWidth(plus);
|
||||||
|
if (spent < 0)
|
||||||
|
{
|
||||||
|
String minus = " -" + CurrencyCard.number(-spent);
|
||||||
|
g.setColor(Ui.DOWN);
|
||||||
|
g.drawString(minus, x, line2);
|
||||||
|
x += sfm.stringWidth(minus);
|
||||||
|
}
|
||||||
|
g.setColor(Ui.FAINT);
|
||||||
|
g.drawString(" " + filter.getLabel(), x, line2);
|
||||||
|
g.setColor(active ? Ui.UP : Ui.FAINT);
|
||||||
|
Ui.drawRight(g, active ? "now" : lastGain > 0 ? CurrencyCard.ago(lastGain) : "", w - pad, line2);
|
||||||
|
g.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String clip(FontMetrics fm, String s, int max)
|
||||||
|
{
|
||||||
|
if (fm.stringWidth(s) <= max)
|
||||||
|
{
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
for (int i = s.length() - 1; i > 0; i--)
|
||||||
|
{
|
||||||
|
String t = s.substring(0, i) + "..";
|
||||||
|
if (fm.stringWidth(t) <= max)
|
||||||
|
{
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Coalesced refresh from any thread. */
|
||||||
|
public void refreshLater()
|
||||||
|
{
|
||||||
|
SwingUtilities.invokeLater(this::refresh);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,462 @@
|
|||||||
|
package com.example.essentialsplus.data;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.WealthCategory;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persisted state of one character. Collections are replaced wholesale (never mutated in place,
|
||||||
|
* except history which is copy-on-write) so the UI and the save thread can read them safely.
|
||||||
|
*/
|
||||||
|
public class AccountData
|
||||||
|
{
|
||||||
|
public int schema = 3;
|
||||||
|
public String key;
|
||||||
|
public long accountHash;
|
||||||
|
public String profile;
|
||||||
|
public String name;
|
||||||
|
public long lastUpdated;
|
||||||
|
public boolean bankKnown;
|
||||||
|
public long[] current;
|
||||||
|
/** running sum of value changes caused only by GE price movements (held quantity x price change) */
|
||||||
|
public long market;
|
||||||
|
|
||||||
|
/** Last known item stacks per source ("bank", "inventory", ...): item id -> quantity. */
|
||||||
|
public Map<String, Map<Integer, Long>> containers = new ConcurrentHashMap<>();
|
||||||
|
public List<SourceValue> sources = new ArrayList<>();
|
||||||
|
public List<TopItem> topItems = new ArrayList<>();
|
||||||
|
public List<Snapshot> history = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
|
/** Grand Exchange slot state (see GrandExchangeTracker) */
|
||||||
|
public GeSlot[] geSlots;
|
||||||
|
/** Items known to be collected already when GE tracking starts for this character (item id -> qty). */
|
||||||
|
public Map<Integer, Long> geSeedCollected;
|
||||||
|
/** Items that went to the bank while it was not visible; dropped when the bank is seen again. */
|
||||||
|
public Map<Integer, Long> bankPending = new java.util.HashMap<>();
|
||||||
|
/** 4-hour Grand Exchange buy limit tracker: canonical item id -> GeBuyLimit */
|
||||||
|
public Map<Integer, GeBuyLimit> geBuyLimits = new ConcurrentHashMap<>();
|
||||||
|
/** Snapshot of Grand Exchange slots for 4h buy limit tracking, modeled after LimitKeeper */
|
||||||
|
public GeBuyLimitSlot[] geBuyLimitSlots = new GeBuyLimitSlot[8];
|
||||||
|
|
||||||
|
/** Timestamp of last right-click Reset Hourly in Wealth Tracker for 'Now' */
|
||||||
|
public long resetHourlyNowAt;
|
||||||
|
/** Timestamp of last right-click Reset Hourly in Wealth Tracker for 'Session' */
|
||||||
|
public long resetHourlySessionAt;
|
||||||
|
|
||||||
|
public static class GeBuyLimitSlot
|
||||||
|
{
|
||||||
|
public int itemId;
|
||||||
|
public int price;
|
||||||
|
public int total;
|
||||||
|
public int quantity;
|
||||||
|
public int spent;
|
||||||
|
|
||||||
|
public GeBuyLimitSlot()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public GeBuyLimitSlot(int itemId, int price, int total, int quantity, int spent)
|
||||||
|
{
|
||||||
|
this.itemId = itemId;
|
||||||
|
this.price = price;
|
||||||
|
this.total = total;
|
||||||
|
this.quantity = quantity;
|
||||||
|
this.spent = spent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSameOffer(GeBuyLimitSlot other)
|
||||||
|
{
|
||||||
|
return other != null && other.itemId == itemId && other.price == price && other.total == total;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public transient long fileModified;
|
||||||
|
|
||||||
|
public long currentTotal()
|
||||||
|
{
|
||||||
|
long sum = 0;
|
||||||
|
if (current != null)
|
||||||
|
{
|
||||||
|
for (long x : current)
|
||||||
|
{
|
||||||
|
sum += x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Snapshot lastSnapshot()
|
||||||
|
{
|
||||||
|
List<Snapshot> h = history;
|
||||||
|
return h.isEmpty() ? null : h.get(h.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void normalize()
|
||||||
|
{
|
||||||
|
containers = containers == null ? new ConcurrentHashMap<>() : new ConcurrentHashMap<>(containers);
|
||||||
|
sources = sources == null ? new ArrayList<>() : sources;
|
||||||
|
topItems = topItems == null ? new ArrayList<>() : topItems;
|
||||||
|
bankPending = bankPending == null ? new java.util.HashMap<>() : bankPending;
|
||||||
|
geBuyLimits = geBuyLimits == null ? new ConcurrentHashMap<>() : new ConcurrentHashMap<>(geBuyLimits);
|
||||||
|
if (geBuyLimitSlots == null || geBuyLimitSlots.length != 8)
|
||||||
|
{
|
||||||
|
geBuyLimitSlots = geBuyLimitSlots == null ? new GeBuyLimitSlot[8] : java.util.Arrays.copyOf(geBuyLimitSlots, 8);
|
||||||
|
}
|
||||||
|
if (schema < 3)
|
||||||
|
{
|
||||||
|
clearBuyLimits();
|
||||||
|
schema = 3;
|
||||||
|
}
|
||||||
|
List<Snapshot> h = history == null ? new ArrayList<>() : history;
|
||||||
|
h.removeIf(s -> s == null);
|
||||||
|
h.forEach(Snapshot::normalize);
|
||||||
|
h.sort((a, b) -> Long.compare(a.t, b.t));
|
||||||
|
h = sanitizeHistory(h);
|
||||||
|
history = new CopyOnWriteArrayList<>(h);
|
||||||
|
if (current != null && current.length < WealthCategory.COUNT)
|
||||||
|
{
|
||||||
|
current = java.util.Arrays.copyOf(current, WealthCategory.COUNT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final long MAX_SPIKE_WINDOW_MS = 5 * 60 * 1000L;
|
||||||
|
public static final long MIN_SPIKE_ABS_GP = 1_000_000L;
|
||||||
|
public static final double MIN_SPIKE_REL_RATIO = 0.08;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a single snapshot at (tSpike, vSpike) represents a transient outlier spike between
|
||||||
|
* (tBefore, vBefore) and (tAfter, vAfter) that jumped away and quickly returned to baseline.
|
||||||
|
*/
|
||||||
|
public static boolean isTransientSpike(long tBefore, long vBefore,
|
||||||
|
long tSpike, long vSpike,
|
||||||
|
long tAfter, long vAfter)
|
||||||
|
{
|
||||||
|
long legBefore = tSpike - tBefore;
|
||||||
|
long legAfter = tAfter - tSpike;
|
||||||
|
if (legBefore <= 0 || legAfter <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The spike itself must be transient:
|
||||||
|
// Either the entire span is within MAX_SPIKE_WINDOW_MS (continuous session),
|
||||||
|
// OR across a session/logout gap (where one leg is long), the spike only persisted for
|
||||||
|
// a brief moment (Math.min(legBefore, legAfter) <= MAX_SPIKE_WINDOW_MS).
|
||||||
|
boolean transientDuration = (tAfter - tBefore <= MAX_SPIKE_WINDOW_MS)
|
||||||
|
|| (Math.min(legBefore, legAfter) <= MAX_SPIKE_WINDOW_MS);
|
||||||
|
if (!transientDuration)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
long d1 = vSpike - vBefore;
|
||||||
|
long d2 = vAfter - vSpike;
|
||||||
|
|
||||||
|
// Must be opposite directions (a jump away and a return)
|
||||||
|
if ((d1 > 0 && d2 >= 0) || (d1 < 0 && d2 <= 0) || d1 == 0 || d2 == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
long absD1 = Math.abs(d1);
|
||||||
|
long absD2 = Math.abs(d2);
|
||||||
|
long spikeMag = Math.min(absD1, absD2);
|
||||||
|
long baseDelta = Math.abs(vAfter - vBefore);
|
||||||
|
|
||||||
|
long baseWealth = Math.max(10_000L, Math.min(Math.abs(vBefore), Math.abs(vAfter)));
|
||||||
|
|
||||||
|
// Magnitude check: must be at least 1M GP or at least 8% of baseline wealth
|
||||||
|
boolean significantSpike = spikeMag >= MIN_SPIKE_ABS_GP || (double) spikeMag / baseWealth >= MIN_SPIKE_REL_RATIO;
|
||||||
|
if (!significantSpike)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Baseline consistency check: after the spike, the value returned close to the starting baseline.
|
||||||
|
return baseDelta <= 0.35 * spikeMag
|
||||||
|
|| baseDelta <= (long) (0.05 * Math.max(Math.abs(vBefore), Math.abs(vAfter)) + 500_000L);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if two consecutive snapshots represent a 2-point transient outlier spike between
|
||||||
|
* (tBefore, vBefore) and (tAfter, vAfter).
|
||||||
|
*/
|
||||||
|
public static boolean isDoubleTransientSpike(long tBefore, long vBefore,
|
||||||
|
long tSpike1, long vSpike1,
|
||||||
|
long tSpike2, long vSpike2,
|
||||||
|
long tAfter, long vAfter)
|
||||||
|
{
|
||||||
|
long legBefore = tSpike1 - tBefore;
|
||||||
|
long legGlitch = tSpike2 - tSpike1;
|
||||||
|
long legAfter = tAfter - tSpike2;
|
||||||
|
if (legBefore <= 0 || legGlitch <= 0 || legAfter <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (legGlitch > MAX_SPIKE_WINDOW_MS)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean transientDuration = (tAfter - tBefore <= MAX_SPIKE_WINDOW_MS)
|
||||||
|
|| (Math.min(legBefore, legAfter) + legGlitch <= MAX_SPIKE_WINDOW_MS);
|
||||||
|
if (!transientDuration)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
long d1_1 = vSpike1 - vBefore;
|
||||||
|
long d1_2 = vSpike2 - vBefore;
|
||||||
|
long d2_1 = vAfter - vSpike1;
|
||||||
|
long d2_2 = vAfter - vSpike2;
|
||||||
|
|
||||||
|
// Both points must deviate in the same direction from baseline and return
|
||||||
|
boolean bothPositive = d1_1 > 0 && d1_2 > 0 && d2_1 < 0 && d2_2 < 0;
|
||||||
|
boolean bothNegative = d1_1 < 0 && d1_2 < 0 && d2_1 > 0 && d2_2 > 0;
|
||||||
|
if (!bothPositive && !bothNegative)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
long spikeMag = Math.min(
|
||||||
|
Math.min(Math.abs(d1_1), Math.abs(d1_2)),
|
||||||
|
Math.min(Math.abs(d2_1), Math.abs(d2_2))
|
||||||
|
);
|
||||||
|
long baseDelta = Math.abs(vAfter - vBefore);
|
||||||
|
long baseWealth = Math.max(10_000L, Math.min(Math.abs(vBefore), Math.abs(vAfter)));
|
||||||
|
|
||||||
|
boolean significantSpike = spikeMag >= MIN_SPIKE_ABS_GP || (double) spikeMag / baseWealth >= MIN_SPIKE_REL_RATIO;
|
||||||
|
if (!significantSpike)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseDelta <= 0.35 * spikeMag
|
||||||
|
|| baseDelta <= (long) (0.05 * Math.max(Math.abs(vBefore), Math.abs(vAfter)) + 500_000L);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if three consecutive snapshots represent a 3-point transient outlier spike between
|
||||||
|
* (tBefore, vBefore) and (tAfter, vAfter).
|
||||||
|
*/
|
||||||
|
public static boolean isTripleTransientSpike(long tBefore, long vBefore,
|
||||||
|
long tSpike1, long vSpike1,
|
||||||
|
long tSpike2, long vSpike2,
|
||||||
|
long tSpike3, long vSpike3,
|
||||||
|
long tAfter, long vAfter)
|
||||||
|
{
|
||||||
|
long legBefore = tSpike1 - tBefore;
|
||||||
|
long legGlitch1 = tSpike2 - tSpike1;
|
||||||
|
long legGlitch2 = tSpike3 - tSpike2;
|
||||||
|
long totalGlitch = tSpike3 - tSpike1;
|
||||||
|
long legAfter = tAfter - tSpike3;
|
||||||
|
if (legBefore <= 0 || legGlitch1 <= 0 || legGlitch2 <= 0 || legAfter <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalGlitch > MAX_SPIKE_WINDOW_MS)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean transientDuration = (tAfter - tBefore <= MAX_SPIKE_WINDOW_MS)
|
||||||
|
|| (Math.min(legBefore, legAfter) + totalGlitch <= MAX_SPIKE_WINDOW_MS);
|
||||||
|
if (!transientDuration)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
long d1_1 = vSpike1 - vBefore;
|
||||||
|
long d1_2 = vSpike2 - vBefore;
|
||||||
|
long d1_3 = vSpike3 - vBefore;
|
||||||
|
long d2_1 = vAfter - vSpike1;
|
||||||
|
long d2_2 = vAfter - vSpike2;
|
||||||
|
long d2_3 = vAfter - vSpike3;
|
||||||
|
|
||||||
|
boolean allPositive = d1_1 > 0 && d1_2 > 0 && d1_3 > 0 && d2_1 < 0 && d2_2 < 0 && d2_3 < 0;
|
||||||
|
boolean allNegative = d1_1 < 0 && d1_2 < 0 && d1_3 < 0 && d2_1 > 0 && d2_2 > 0 && d2_3 > 0;
|
||||||
|
if (!allPositive && !allNegative)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
long spikeMag = Math.min(
|
||||||
|
Math.min(Math.abs(d1_1), Math.min(Math.abs(d1_2), Math.abs(d1_3))),
|
||||||
|
Math.min(Math.abs(d2_1), Math.min(Math.abs(d2_2), Math.abs(d2_3)))
|
||||||
|
);
|
||||||
|
long baseDelta = Math.abs(vAfter - vBefore);
|
||||||
|
long baseWealth = Math.max(10_000L, Math.min(Math.abs(vBefore), Math.abs(vAfter)));
|
||||||
|
|
||||||
|
boolean significantSpike = spikeMag >= MIN_SPIKE_ABS_GP || (double) spikeMag / baseWealth >= MIN_SPIKE_REL_RATIO;
|
||||||
|
if (!significantSpike)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseDelta <= 0.35 * spikeMag
|
||||||
|
|| baseDelta <= (long) (0.05 * Math.max(Math.abs(vBefore), Math.abs(vAfter)) + 500_000L);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters out momentary outlier spikes from the timeline (e.g. 1-minute spikes caused by
|
||||||
|
* transient container reloading, temporary GE duplication, or pricing glitches).
|
||||||
|
*/
|
||||||
|
public static List<Snapshot> sanitizeHistory(List<Snapshot> snapshots)
|
||||||
|
{
|
||||||
|
if (snapshots == null || snapshots.size() < 3)
|
||||||
|
{
|
||||||
|
return snapshots == null ? new ArrayList<>() : new ArrayList<>(snapshots);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Snapshot> list = new ArrayList<>(snapshots);
|
||||||
|
boolean changed = true;
|
||||||
|
int passes = 0;
|
||||||
|
while (changed && passes < 5)
|
||||||
|
{
|
||||||
|
changed = false;
|
||||||
|
passes++;
|
||||||
|
|
||||||
|
// Pass 1: check single-point spikes
|
||||||
|
for (int i = 1; i < list.size() - 1; i++)
|
||||||
|
{
|
||||||
|
Snapshot before = list.get(i - 1);
|
||||||
|
Snapshot cur = list.get(i);
|
||||||
|
Snapshot after = list.get(i + 1);
|
||||||
|
|
||||||
|
if (isTransientSpike(before.t, before.total(), cur.t, cur.total(), after.t, after.total()))
|
||||||
|
{
|
||||||
|
list.remove(i);
|
||||||
|
changed = true;
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 2: check two-point spikes
|
||||||
|
for (int i = 1; i < list.size() - 2; i++)
|
||||||
|
{
|
||||||
|
Snapshot before = list.get(i - 1);
|
||||||
|
Snapshot s1 = list.get(i);
|
||||||
|
Snapshot s2 = list.get(i + 1);
|
||||||
|
Snapshot after = list.get(i + 2);
|
||||||
|
|
||||||
|
if (isDoubleTransientSpike(before.t, before.total(), s1.t, s1.total(), s2.t, s2.total(), after.t, after.total()))
|
||||||
|
{
|
||||||
|
list.remove(i + 1);
|
||||||
|
list.remove(i);
|
||||||
|
changed = true;
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 3: check three-point spikes
|
||||||
|
for (int i = 1; i < list.size() - 3; i++)
|
||||||
|
{
|
||||||
|
Snapshot before = list.get(i - 1);
|
||||||
|
Snapshot s1 = list.get(i);
|
||||||
|
Snapshot s2 = list.get(i + 1);
|
||||||
|
Snapshot s3 = list.get(i + 2);
|
||||||
|
Snapshot after = list.get(i + 3);
|
||||||
|
|
||||||
|
if (isTripleTransientSpike(before.t, before.total(), s1.t, s1.total(), s2.t, s2.total(), s3.t, s3.total(), after.t, after.total()))
|
||||||
|
{
|
||||||
|
list.remove(i + 2);
|
||||||
|
list.remove(i + 1);
|
||||||
|
list.remove(i);
|
||||||
|
changed = true;
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final long FOUR_HOURS_MS = 4 * 3600 * 1000L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records a purchase of an item, managing the 4-hour window rollover.
|
||||||
|
*/
|
||||||
|
public void recordBuy(int itemId, int quantity, long now)
|
||||||
|
{
|
||||||
|
if (quantity <= 0 || itemId <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (geBuyLimits == null)
|
||||||
|
{
|
||||||
|
geBuyLimits = new ConcurrentHashMap<>();
|
||||||
|
}
|
||||||
|
GeBuyLimit limit = geBuyLimits.computeIfAbsent(itemId, id -> new GeBuyLimit(id, 0, 0, 0));
|
||||||
|
synchronized (limit)
|
||||||
|
{
|
||||||
|
if (limit.windowStart == 0 || now - limit.windowStart >= FOUR_HOURS_MS)
|
||||||
|
{
|
||||||
|
limit.windowStart = now;
|
||||||
|
limit.bought = quantity;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
limit.bought += quantity;
|
||||||
|
}
|
||||||
|
limit.lastTradeTime = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the current 4-hour buy limit status for an item.
|
||||||
|
*/
|
||||||
|
public GeLimitInfo getBuyLimitInfo(int itemId, int itemGeLimit, long now)
|
||||||
|
{
|
||||||
|
GeBuyLimit limit = geBuyLimits != null ? geBuyLimits.get(itemId) : null;
|
||||||
|
if (limit == null || limit.windowStart == 0)
|
||||||
|
{
|
||||||
|
return new GeLimitInfo(itemId, itemGeLimit, 0, itemGeLimit, 0, false);
|
||||||
|
}
|
||||||
|
synchronized (limit)
|
||||||
|
{
|
||||||
|
long elapsed = now - limit.windowStart;
|
||||||
|
if (elapsed >= FOUR_HOURS_MS)
|
||||||
|
{
|
||||||
|
// Expired: full limit restored
|
||||||
|
return new GeLimitInfo(itemId, itemGeLimit, 0, itemGeLimit, 0, false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int bought = limit.bought;
|
||||||
|
int remaining = Math.max(0, itemGeLimit > 0 ? itemGeLimit - bought : 0);
|
||||||
|
long resetInMs = FOUR_HOURS_MS - elapsed;
|
||||||
|
return new GeLimitInfo(itemId, itemGeLimit, bought, remaining, resetInMs, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets all tracked 4-hour buy limits and slot tracking for this character.
|
||||||
|
*/
|
||||||
|
public void clearBuyLimits()
|
||||||
|
{
|
||||||
|
if (geBuyLimits != null)
|
||||||
|
{
|
||||||
|
geBuyLimits.clear();
|
||||||
|
}
|
||||||
|
geBuyLimitSlots = new GeBuyLimitSlot[8];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetHourly(boolean isSession)
|
||||||
|
{
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
if (isSession)
|
||||||
|
{
|
||||||
|
resetHourlySessionAt = now;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
resetHourlyNowAt = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.example.essentialsplus.data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks purchases of a specific item within the rolling 4-hour Grand Exchange buy limit window.
|
||||||
|
*/
|
||||||
|
public class GeBuyLimit
|
||||||
|
{
|
||||||
|
public int itemId;
|
||||||
|
public int bought;
|
||||||
|
public long windowStart;
|
||||||
|
public long lastTradeTime;
|
||||||
|
|
||||||
|
public GeBuyLimit()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public GeBuyLimit(int itemId, int bought, long windowStart, long lastTradeTime)
|
||||||
|
{
|
||||||
|
this.itemId = itemId;
|
||||||
|
this.bought = bought;
|
||||||
|
this.windowStart = windowStart;
|
||||||
|
this.lastTradeTime = lastTradeTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.example.essentialsplus.data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculated status of an item's 4-hour Grand Exchange buy limit.
|
||||||
|
*/
|
||||||
|
public class GeLimitInfo
|
||||||
|
{
|
||||||
|
public final int itemId;
|
||||||
|
public final int limit;
|
||||||
|
public final int bought;
|
||||||
|
public final int remaining;
|
||||||
|
public final long resetInMs;
|
||||||
|
public final boolean windowActive;
|
||||||
|
|
||||||
|
public GeLimitInfo(int itemId, int limit, int bought, int remaining, long resetInMs, boolean windowActive)
|
||||||
|
{
|
||||||
|
this.itemId = itemId;
|
||||||
|
this.limit = limit;
|
||||||
|
this.bought = bought;
|
||||||
|
this.remaining = remaining;
|
||||||
|
this.resetInMs = resetInMs;
|
||||||
|
this.windowActive = windowActive;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.example.essentialsplus.data;
|
||||||
|
|
||||||
|
/** One Grand Exchange slot prepared for display. */
|
||||||
|
public class GeRow
|
||||||
|
{
|
||||||
|
public final String character;
|
||||||
|
public final int slot;
|
||||||
|
/** null = empty slot */
|
||||||
|
public final GeSlot offer;
|
||||||
|
public final String name;
|
||||||
|
public final int marketPrice;
|
||||||
|
public final GeLimitInfo limitInfo;
|
||||||
|
|
||||||
|
public GeRow(String character, int slot, GeSlot offer, String name, int marketPrice)
|
||||||
|
{
|
||||||
|
this(character, slot, offer, name, marketPrice, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GeRow(String character, int slot, GeSlot offer, String name, int marketPrice, GeLimitInfo limitInfo)
|
||||||
|
{
|
||||||
|
this.character = character;
|
||||||
|
this.slot = slot;
|
||||||
|
this.offer = offer;
|
||||||
|
this.name = name;
|
||||||
|
this.marketPrice = marketPrice;
|
||||||
|
this.limitInfo = limitInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.example.essentialsplus.data;
|
||||||
|
|
||||||
|
/** Last known state of one Grand Exchange slot, persisted so collections can be told apart from gains. */
|
||||||
|
public class GeSlot
|
||||||
|
{
|
||||||
|
public int item;
|
||||||
|
public boolean buy;
|
||||||
|
public long total;
|
||||||
|
public long price;
|
||||||
|
|
||||||
|
/** items / coins already taken out of the collection box (fallback tracking) */
|
||||||
|
public long collectedItems;
|
||||||
|
public long collectedCoins;
|
||||||
|
|
||||||
|
/** value held by the slot at the last update */
|
||||||
|
public long escrowItems;
|
||||||
|
public long escrowCoins;
|
||||||
|
public long collectItems;
|
||||||
|
public long collectCoins;
|
||||||
|
|
||||||
|
/** live offer details */
|
||||||
|
public long done;
|
||||||
|
public long spent;
|
||||||
|
public String state;
|
||||||
|
/** when the offer was placed (0 = before tracking started, see trackedSince) */
|
||||||
|
public long placedAt;
|
||||||
|
public long trackedSince;
|
||||||
|
public long trackedDone;
|
||||||
|
public long lastTradeAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.example.essentialsplus.data;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
|
/** Item holdings over time, stored separately from the wealth timeline (<key>.items.json). */
|
||||||
|
public class ItemLedger
|
||||||
|
{
|
||||||
|
public List<ItemSnapshot> snapshots = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
|
public transient long fileModified;
|
||||||
|
public transient boolean dirty;
|
||||||
|
|
||||||
|
public ItemSnapshot last()
|
||||||
|
{
|
||||||
|
List<ItemSnapshot> s = snapshots;
|
||||||
|
return s.isEmpty() ? null : s.get(s.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Latest snapshot taken at or before {@code t}, or the first one after it if none exists. */
|
||||||
|
public ItemSnapshot at(long t)
|
||||||
|
{
|
||||||
|
List<ItemSnapshot> s = snapshots;
|
||||||
|
ItemSnapshot best = null;
|
||||||
|
for (ItemSnapshot x : s)
|
||||||
|
{
|
||||||
|
if (x.t <= t)
|
||||||
|
{
|
||||||
|
best = x;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return best != null ? best : x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void normalize()
|
||||||
|
{
|
||||||
|
List<ItemSnapshot> s = snapshots == null ? new ArrayList<>() : new ArrayList<>(snapshots);
|
||||||
|
s.removeIf(x -> x == null || x.ids == null || x.q == null || x.p == null
|
||||||
|
|| x.ids.length != x.q.length || x.ids.length != x.p.length);
|
||||||
|
s.sort((a, b) -> Long.compare(a.t, b.t));
|
||||||
|
snapshots = new CopyOnWriteArrayList<>(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.example.essentialsplus.data;
|
||||||
|
|
||||||
|
/** One priced item stack for the item viewer. */
|
||||||
|
public class ItemRow
|
||||||
|
{
|
||||||
|
public final int id;
|
||||||
|
public final String name;
|
||||||
|
public final long qty;
|
||||||
|
public final long price;
|
||||||
|
public final long value;
|
||||||
|
|
||||||
|
public ItemRow(int id, String name, long qty, long price)
|
||||||
|
{
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
this.qty = qty;
|
||||||
|
this.price = price;
|
||||||
|
this.value = qty * price;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.example.essentialsplus.data;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holdings of a character at one moment: canonical item id, quantity and the GE price at that time.
|
||||||
|
* Allows splitting wealth changes into "market" (price) and "activity" (quantity) effects.
|
||||||
|
*/
|
||||||
|
public class ItemSnapshot
|
||||||
|
{
|
||||||
|
public long t;
|
||||||
|
public int[] ids;
|
||||||
|
public long[] q;
|
||||||
|
public int[] p;
|
||||||
|
/** pinned (session start) - kept by compaction */
|
||||||
|
public Boolean k;
|
||||||
|
|
||||||
|
public ItemSnapshot()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemSnapshot(long t, int[] ids, long[] q, int[] p)
|
||||||
|
{
|
||||||
|
this.t = t;
|
||||||
|
this.ids = ids;
|
||||||
|
this.q = q;
|
||||||
|
this.p = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean sameAs(ItemSnapshot other)
|
||||||
|
{
|
||||||
|
return other != null && Arrays.equals(ids, other.ids) && Arrays.equals(q, other.q) && Arrays.equals(p, other.p);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean sameQuantities(ItemSnapshot other)
|
||||||
|
{
|
||||||
|
return other != null && Arrays.equals(ids, other.ids) && Arrays.equals(q, other.q);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int indexOf(int id)
|
||||||
|
{
|
||||||
|
return ids == null ? -1 : Arrays.binarySearch(ids, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
package com.example.essentialsplus.data;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.WealthCategory;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One point on the wealth timeline.
|
||||||
|
* <p>
|
||||||
|
* Raw points only have {@code t} and {@code v}. Aggregated points (hourly / daily buckets of older data)
|
||||||
|
* additionally hold open, close, low and high of the total and store the time-weighted average
|
||||||
|
* breakdown in {@code v}.
|
||||||
|
*/
|
||||||
|
public class Snapshot
|
||||||
|
{
|
||||||
|
public static final String HOUR = "h";
|
||||||
|
public static final String DAY = "d";
|
||||||
|
|
||||||
|
public long t;
|
||||||
|
public long[] v;
|
||||||
|
/** open / close / low / high total - only set on aggregates */
|
||||||
|
public Long o;
|
||||||
|
public Long c;
|
||||||
|
public Long lo;
|
||||||
|
public Long hi;
|
||||||
|
/** granularity of an aggregate: "h" or "d" */
|
||||||
|
public String g;
|
||||||
|
/** cumulative value change from GE price movements up to this point ({@link AccountData#market}); null on old data */
|
||||||
|
public Long m;
|
||||||
|
|
||||||
|
public Snapshot()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Snapshot(long t, long[] v)
|
||||||
|
{
|
||||||
|
this.t = t;
|
||||||
|
this.v = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long total()
|
||||||
|
{
|
||||||
|
long sum = 0;
|
||||||
|
if (v != null)
|
||||||
|
{
|
||||||
|
for (long x : v)
|
||||||
|
{
|
||||||
|
sum += x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long get(WealthCategory category)
|
||||||
|
{
|
||||||
|
int i = category.ordinal();
|
||||||
|
return v != null && i < v.length ? v[i] : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long low()
|
||||||
|
{
|
||||||
|
return lo != null ? lo : total();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long high()
|
||||||
|
{
|
||||||
|
return hi != null ? hi : total();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long open()
|
||||||
|
{
|
||||||
|
return o != null ? o : total();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long close()
|
||||||
|
{
|
||||||
|
return c != null ? c : total();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAggregate()
|
||||||
|
{
|
||||||
|
return g != null || lo != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pads arrays loaded from older files to the current category count. */
|
||||||
|
public void normalize()
|
||||||
|
{
|
||||||
|
if (v == null)
|
||||||
|
{
|
||||||
|
v = new long[WealthCategory.COUNT];
|
||||||
|
}
|
||||||
|
else if (v.length < WealthCategory.COUNT)
|
||||||
|
{
|
||||||
|
v = Arrays.copyOf(v, WealthCategory.COUNT);
|
||||||
|
}
|
||||||
|
if (lo != null && g == null)
|
||||||
|
{
|
||||||
|
g = DAY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public long market()
|
||||||
|
{
|
||||||
|
return m != null ? m : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Copy with {@code delta} removed from the total, spread over the categories by their share. */
|
||||||
|
public Snapshot shifted(long delta)
|
||||||
|
{
|
||||||
|
long[] nv = v == null ? new long[0] : v.clone();
|
||||||
|
if (delta != 0 && nv.length > 0)
|
||||||
|
{
|
||||||
|
long base = 0;
|
||||||
|
for (long x : nv)
|
||||||
|
{
|
||||||
|
base += Math.abs(x);
|
||||||
|
}
|
||||||
|
long left = delta;
|
||||||
|
int largest = 0;
|
||||||
|
for (int i = 0; i < nv.length; i++)
|
||||||
|
{
|
||||||
|
if (Math.abs(nv[i]) > Math.abs(nv[largest]))
|
||||||
|
{
|
||||||
|
largest = i;
|
||||||
|
}
|
||||||
|
if (base > 0)
|
||||||
|
{
|
||||||
|
long part = Math.round((double) delta * Math.abs(nv[i]) / base);
|
||||||
|
nv[i] -= part;
|
||||||
|
left -= part;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nv[largest] -= left;
|
||||||
|
}
|
||||||
|
Snapshot s = new Snapshot(t, nv);
|
||||||
|
s.g = g;
|
||||||
|
s.m = m;
|
||||||
|
s.o = o == null ? null : o - delta;
|
||||||
|
s.c = c == null ? null : c - delta;
|
||||||
|
s.lo = lo == null ? null : lo - delta;
|
||||||
|
s.hi = hi == null ? null : hi - delta;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean sameValues(long[] other)
|
||||||
|
{
|
||||||
|
return Arrays.equals(v, other);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.example.essentialsplus.data;
|
||||||
|
|
||||||
|
/** Value of a single wealth source (e.g. "Looting bag") inside a category. */
|
||||||
|
public class SourceValue
|
||||||
|
{
|
||||||
|
public String name;
|
||||||
|
public int category;
|
||||||
|
public long value;
|
||||||
|
|
||||||
|
public SourceValue()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public SourceValue(String name, int category, long value)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
this.category = category;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.example.essentialsplus.data;
|
||||||
|
|
||||||
|
public class TopItem
|
||||||
|
{
|
||||||
|
public int id;
|
||||||
|
public String name;
|
||||||
|
public long qty;
|
||||||
|
public long value;
|
||||||
|
|
||||||
|
public TopItem()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public TopItem(int id, String name, long qty, long value)
|
||||||
|
{
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
this.qty = qty;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
package com.example.essentialsplus.helpers;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.EssentialsPlusConfig;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.WeakHashMap;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import net.runelite.api.Actor;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.GameState;
|
||||||
|
import net.runelite.api.NPC;
|
||||||
|
import net.runelite.api.Player;
|
||||||
|
import net.runelite.api.Projectile;
|
||||||
|
import net.runelite.api.events.AnimationChanged;
|
||||||
|
import net.runelite.api.events.GameStateChanged;
|
||||||
|
import net.runelite.api.events.GameTick;
|
||||||
|
import net.runelite.api.events.NpcDespawned;
|
||||||
|
import net.runelite.api.events.ProjectileMoved;
|
||||||
|
import net.runelite.api.gameval.AnimationID;
|
||||||
|
import net.runelite.api.gameval.SpotanimID;
|
||||||
|
import net.runelite.client.eventbus.Subscribe;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Which style each NPC attacking the local player used last. The style comes from the projectile (by its cache name,
|
||||||
|
* e.g. {@code CRYSTAL_HUNLLEF_RANGE_TRAVEL}), else the attack animation name, else melee when the NPC attacked from
|
||||||
|
* melee distance without a projectile.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
public class AttackStyles
|
||||||
|
{
|
||||||
|
/** How long a style stays shown after the NPC's last attack. */
|
||||||
|
private static final int SHOW_TICKS = 12;
|
||||||
|
/** Projectiles whose names do not say the style. */
|
||||||
|
private static final Map<Integer, CombatStyle> PROJECTILE_OVERRIDES = Map.of(
|
||||||
|
SpotanimID.CRYSTAL_HUNLLEF_PRAYER_TRAVEL, CombatStyle.MAGIC,
|
||||||
|
SpotanimID.CRYSTAL_HUNLLEF_PRAYER_TRAVEL_HM, CombatStyle.MAGIC,
|
||||||
|
SpotanimID.ECHO_HUNLLEF_ANTIPRAYER_TRAVEL, CombatStyle.MAGIC);
|
||||||
|
|
||||||
|
private final Client client;
|
||||||
|
private final EssentialsPlusConfig config;
|
||||||
|
|
||||||
|
private Map<Integer, CombatStyle> projectileStyles;
|
||||||
|
private Map<Integer, CombatStyle> animationStyles;
|
||||||
|
|
||||||
|
private final Map<NPC, Shown> styles = new HashMap<>();
|
||||||
|
/** Attack animations without a known style: melee if no projectile follows by the next tick. */
|
||||||
|
private final Map<NPC, Integer> unresolved = new HashMap<>();
|
||||||
|
private final Set<Projectile> seenProjectiles = Collections.newSetFromMap(new WeakHashMap<>());
|
||||||
|
|
||||||
|
private static final class Shown
|
||||||
|
{
|
||||||
|
final CombatStyle style;
|
||||||
|
final int tick;
|
||||||
|
|
||||||
|
Shown(CombatStyle style, int tick)
|
||||||
|
{
|
||||||
|
this.style = style;
|
||||||
|
this.tick = tick;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
AttackStyles(Client client, EssentialsPlusConfig config)
|
||||||
|
{
|
||||||
|
this.client = client;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Last attack style of every NPC currently attacking the local player. */
|
||||||
|
public Map<NPC, CombatStyle> current()
|
||||||
|
{
|
||||||
|
Map<NPC, CombatStyle> out = new HashMap<>();
|
||||||
|
Player me = client.getLocalPlayer();
|
||||||
|
int tick = client.getTickCount();
|
||||||
|
for (Map.Entry<NPC, Shown> e : styles.entrySet())
|
||||||
|
{
|
||||||
|
NPC npc = e.getKey();
|
||||||
|
if (me != null && !npc.isDead() && npc.getInteracting() == me && tick - e.getValue().tick <= SHOW_TICKS)
|
||||||
|
{
|
||||||
|
out.put(npc, e.getValue().style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onAnimationChanged(AnimationChanged event)
|
||||||
|
{
|
||||||
|
if (!config.enemyAttackStyle() || !(event.getActor() instanceof NPC))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NPC npc = (NPC) event.getActor();
|
||||||
|
int anim = npc.getAnimation();
|
||||||
|
Player me = client.getLocalPlayer();
|
||||||
|
if (anim == -1 || me == null || npc.getInteracting() != me || npc.isDead())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CombatStyle style = animationStyles().get(anim);
|
||||||
|
if (style != null)
|
||||||
|
{
|
||||||
|
set(npc, style);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
unresolved.putIfAbsent(npc, client.getTickCount());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onProjectileMoved(ProjectileMoved event)
|
||||||
|
{
|
||||||
|
if (!config.enemyAttackStyle())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Projectile p = event.getProjectile();
|
||||||
|
if (!seenProjectiles.add(p))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Player me = client.getLocalPlayer();
|
||||||
|
Actor target = p.getTargetActor();
|
||||||
|
if (me == null || target != null && target != me)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NPC source = p.getSourceActor() instanceof NPC ? (NPC) p.getSourceActor() : null;
|
||||||
|
if (source == null)
|
||||||
|
{
|
||||||
|
// no source: the NPC that just animated at us
|
||||||
|
for (NPC npc : unresolved.keySet())
|
||||||
|
{
|
||||||
|
source = npc;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (source == null || source.getInteracting() != me)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unresolved.remove(source);
|
||||||
|
CombatStyle style = PROJECTILE_OVERRIDES.getOrDefault(p.getId(), projectileStyles().get(p.getId()));
|
||||||
|
if (style != null)
|
||||||
|
{
|
||||||
|
set(source, style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onGameTick(GameTick event)
|
||||||
|
{
|
||||||
|
int tick = client.getTickCount();
|
||||||
|
Player me = client.getLocalPlayer();
|
||||||
|
unresolved.entrySet().removeIf(e ->
|
||||||
|
{
|
||||||
|
if (tick - e.getValue() < 1)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
NPC npc = e.getKey();
|
||||||
|
if (me != null && npc.getWorldArea() != null && me.getWorldArea() != null
|
||||||
|
&& npc.getWorldArea().isInMeleeDistance(me.getWorldArea()))
|
||||||
|
{
|
||||||
|
set(npc, CombatStyle.MELEE);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
styles.values().removeIf(s -> tick - s.tick > SHOW_TICKS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onNpcDespawned(NpcDespawned event)
|
||||||
|
{
|
||||||
|
styles.remove(event.getNpc());
|
||||||
|
unresolved.remove(event.getNpc());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onGameStateChanged(GameStateChanged event)
|
||||||
|
{
|
||||||
|
if (event.getGameState() != GameState.LOGGED_IN)
|
||||||
|
{
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset()
|
||||||
|
{
|
||||||
|
styles.clear();
|
||||||
|
unresolved.clear();
|
||||||
|
seenProjectiles.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void set(NPC npc, CombatStyle style)
|
||||||
|
{
|
||||||
|
styles.put(npc, new Shown(style, client.getTickCount()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Integer, CombatStyle> projectileStyles()
|
||||||
|
{
|
||||||
|
if (projectileStyles == null)
|
||||||
|
{
|
||||||
|
projectileStyles = stylesByName(SpotanimID.class);
|
||||||
|
}
|
||||||
|
return projectileStyles;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Integer, CombatStyle> animationStyles()
|
||||||
|
{
|
||||||
|
if (animationStyles == null)
|
||||||
|
{
|
||||||
|
animationStyles = stylesByName(AnimationID.class);
|
||||||
|
}
|
||||||
|
return animationStyles;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Id -> style from the constant names of a gameval class; ids whose names disagree are left out. */
|
||||||
|
static Map<Integer, CombatStyle> stylesByName(Class<?> gameval)
|
||||||
|
{
|
||||||
|
Map<Integer, CombatStyle> out = new HashMap<>();
|
||||||
|
Set<Integer> conflicts = new java.util.HashSet<>();
|
||||||
|
for (Field f : gameval.getFields())
|
||||||
|
{
|
||||||
|
if (f.getType() != int.class || !Modifier.isStatic(f.getModifiers()))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
CombatStyle style = CombatStyle.fromName(f.getName());
|
||||||
|
if (style == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int id = f.getInt(null);
|
||||||
|
CombatStyle prev = out.putIfAbsent(id, style);
|
||||||
|
if (prev != null && prev != style)
|
||||||
|
{
|
||||||
|
conflicts.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IllegalAccessException ignored)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.keySet().removeAll(conflicts);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
package com.example.essentialsplus.helpers;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.EssentialsPlusConfig;
|
||||||
|
import java.awt.BasicStroke;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.FontMetrics;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.RenderingHints;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import net.runelite.api.Actor;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.NPC;
|
||||||
|
import net.runelite.api.Player;
|
||||||
|
import net.runelite.api.Point;
|
||||||
|
import net.runelite.client.game.SpriteManager;
|
||||||
|
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.util.ImageUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Countdown above the player's (and attacking NPCs') head, optionally with one pip per tick of the attack speed,
|
||||||
|
* and the style attacking NPCs used last (prayer icon or text) left of their number.
|
||||||
|
*/
|
||||||
|
public class AttackTimerOverlay extends Overlay
|
||||||
|
{
|
||||||
|
private static final int PIP = 8;
|
||||||
|
private static final int GAP = 2;
|
||||||
|
/** Height of an overhead prayer icon; the player's own timer moves above it. */
|
||||||
|
private static final int OVERHEAD_SHIFT = 26;
|
||||||
|
private static final Color EMPTY = new Color(0, 0, 0, 150);
|
||||||
|
private static final Color OUTLINE = new Color(0, 0, 0, 220);
|
||||||
|
|
||||||
|
private final Client client;
|
||||||
|
private final EssentialsPlusConfig config;
|
||||||
|
private final AttackTimers timers;
|
||||||
|
private final AttackStyles styles;
|
||||||
|
private final SpriteManager spriteManager;
|
||||||
|
/** Scaled prayer icons by size, then style. */
|
||||||
|
private final Map<Integer, Map<CombatStyle, BufferedImage>> icons = new HashMap<>();
|
||||||
|
|
||||||
|
/** Look of one timer kind, read from the config once per frame. */
|
||||||
|
private static final class Style
|
||||||
|
{
|
||||||
|
final boolean bar;
|
||||||
|
final TimerTextSize size;
|
||||||
|
final Color color;
|
||||||
|
final Color ready;
|
||||||
|
final int offset;
|
||||||
|
|
||||||
|
Style(boolean bar, TimerTextSize size, Color color, Color ready, int offset)
|
||||||
|
{
|
||||||
|
this.bar = bar;
|
||||||
|
this.size = size;
|
||||||
|
this.color = color;
|
||||||
|
this.ready = ready;
|
||||||
|
this.offset = offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
AttackTimerOverlay(Client client, EssentialsPlusConfig config, AttackTimers timers, AttackStyles styles, SpriteManager spriteManager)
|
||||||
|
{
|
||||||
|
this.client = client;
|
||||||
|
this.config = config;
|
||||||
|
this.timers = timers;
|
||||||
|
this.styles = styles;
|
||||||
|
this.spriteManager = spriteManager;
|
||||||
|
setPosition(OverlayPosition.DYNAMIC);
|
||||||
|
setLayer(OverlayLayer.ABOVE_SCENE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension render(Graphics2D g)
|
||||||
|
{
|
||||||
|
boolean styleOn = config.enemyAttackStyle();
|
||||||
|
if (!config.attackTimer() && !config.enemyAttackTimer() && !styleOn)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
timers.onRender();
|
||||||
|
if (config.attackTimer())
|
||||||
|
{
|
||||||
|
Player me = client.getLocalPlayer();
|
||||||
|
AttackTimers.Timer t = timers.playerTimer();
|
||||||
|
if (me != null && t != null)
|
||||||
|
{
|
||||||
|
Style style = new Style(config.attackTimerBar(), config.attackTimerSize(), config.attackTimerColor(),
|
||||||
|
config.attackTimerReadyColor(), config.attackTimerOffset());
|
||||||
|
draw(g, me, t, style, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<NPC, AttackTimers.Timer> npcTimers = config.enemyAttackTimer() ? timers.npcTimers() : Map.of();
|
||||||
|
Map<NPC, CombatStyle> npcStyles = styleOn ? styles.current() : Map.of();
|
||||||
|
if (!npcTimers.isEmpty() || !npcStyles.isEmpty())
|
||||||
|
{
|
||||||
|
Style style = new Style(config.enemyAttackTimerBar(), config.enemyAttackTimerSize(), config.enemyAttackTimerColor(),
|
||||||
|
config.enemyAttackTimerReadyColor(), config.enemyAttackTimerOffset());
|
||||||
|
Set<NPC> npcs = new LinkedHashSet<>(npcTimers.keySet());
|
||||||
|
npcs.addAll(npcStyles.keySet());
|
||||||
|
for (NPC npc : npcs)
|
||||||
|
{
|
||||||
|
draw(g, npc, npcTimers.get(npc), style, npcStyles.get(npc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void draw(Graphics2D g, Actor actor, AttackTimers.Timer t, Style style, CombatStyle attackStyle)
|
||||||
|
{
|
||||||
|
if (t != null)
|
||||||
|
{
|
||||||
|
drawTimer(g, actor, t, style);
|
||||||
|
}
|
||||||
|
if (attackStyle != null)
|
||||||
|
{
|
||||||
|
drawStyle(g, actor, attackStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawTimer(Graphics2D g, Actor actor, AttackTimers.Timer t, Style style)
|
||||||
|
{
|
||||||
|
Point anchor = actor.getCanvasTextLocation(g, "", actor.getLogicalHeight() + style.offset);
|
||||||
|
if (anchor == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int y = anchor.getY();
|
||||||
|
if (actor instanceof Player && ((Player) actor).getOverheadIcon() != null)
|
||||||
|
{
|
||||||
|
y -= OVERHEAD_SHIFT;
|
||||||
|
}
|
||||||
|
boolean ready = t.remaining == 0;
|
||||||
|
|
||||||
|
if (style.bar)
|
||||||
|
{
|
||||||
|
int width = t.speed * PIP + (t.speed - 1) * GAP;
|
||||||
|
int x0 = anchor.getX() - width / 2;
|
||||||
|
int barY = y - PIP;
|
||||||
|
// the pip of the running tick drains smoothly; later ticks stay full
|
||||||
|
double fill = t.remaining - t.tickProgress;
|
||||||
|
g.setStroke(new BasicStroke(1f));
|
||||||
|
for (int i = 0; i < t.speed; i++)
|
||||||
|
{
|
||||||
|
int x = x0 + i * (PIP + GAP);
|
||||||
|
g.setColor(EMPTY);
|
||||||
|
g.fillRect(x, barY, PIP, PIP);
|
||||||
|
double part = Math.max(0, Math.min(1, fill - i));
|
||||||
|
if (part > 0)
|
||||||
|
{
|
||||||
|
g.setColor(style.color);
|
||||||
|
g.fillRect(x, barY, (int) Math.round(PIP * part), PIP);
|
||||||
|
}
|
||||||
|
g.setColor(ready ? style.ready : OUTLINE);
|
||||||
|
g.drawRect(x, barY, PIP, PIP);
|
||||||
|
}
|
||||||
|
y = barY - 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
g.setFont(style.size.font());
|
||||||
|
String text = String.valueOf(t.remaining);
|
||||||
|
int x = anchor.getX() - g.getFontMetrics().stringWidth(text) / 2;
|
||||||
|
shadowed(g, text, x, y, ready ? style.ready : style.color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Attack style with its own size and height, centered above the head. */
|
||||||
|
private void drawStyle(Graphics2D g, Actor actor, CombatStyle attackStyle)
|
||||||
|
{
|
||||||
|
Point anchor = actor.getCanvasTextLocation(g, "", actor.getLogicalHeight() + config.enemyAttackStyleOffset());
|
||||||
|
if (anchor == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TimerTextSize size = config.enemyAttackStyleSize();
|
||||||
|
if (config.enemyAttackStyleText())
|
||||||
|
{
|
||||||
|
g.setFont(size.font());
|
||||||
|
int x = anchor.getX() - g.getFontMetrics().stringWidth(attackStyle.label) / 2;
|
||||||
|
shadowed(g, attackStyle.label, x, anchor.getY(), attackStyle.color);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BufferedImage icon = icon(attackStyle, size.iconSize());
|
||||||
|
if (icon != null)
|
||||||
|
{
|
||||||
|
g.drawImage(icon, anchor.getX() - icon.getWidth() / 2, anchor.getY() - icon.getHeight(), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void shadowed(Graphics2D g, String text, int x, int y, Color color)
|
||||||
|
{
|
||||||
|
g.setColor(Color.BLACK);
|
||||||
|
g.drawString(text, x + 1, y + 1);
|
||||||
|
g.setColor(color);
|
||||||
|
g.drawString(text, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BufferedImage icon(CombatStyle style, int size)
|
||||||
|
{
|
||||||
|
Map<CombatStyle, BufferedImage> bySize = icons.computeIfAbsent(size, k -> new EnumMap<>(CombatStyle.class));
|
||||||
|
BufferedImage img = bySize.get(style);
|
||||||
|
if (img == null)
|
||||||
|
{
|
||||||
|
BufferedImage sprite = spriteManager.getSprite(style.prayerSprite, 0);
|
||||||
|
if (sprite == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
img = ImageUtil.resizeImage(sprite, size, size);
|
||||||
|
bySize.put(style, img);
|
||||||
|
}
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,565 @@
|
|||||||
|
package com.example.essentialsplus.helpers;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.EssentialsPlusConfig;
|
||||||
|
import com.example.essentialsplus.helpers.combat.AnimationData;
|
||||||
|
import com.example.essentialsplus.helpers.combat.AttackSpeed;
|
||||||
|
import com.example.essentialsplus.helpers.combat.Attacking;
|
||||||
|
import com.example.essentialsplus.helpers.combat.CombatUtils;
|
||||||
|
import com.example.essentialsplus.helpers.combat.Spellbook;
|
||||||
|
import com.example.essentialsplus.helpers.combat.variablespeed.VariableSpeed;
|
||||||
|
import com.example.essentialsplus.helpers.combat.variablespeed.state.TickCount;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.runelite.api.Actor;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.GameState;
|
||||||
|
import net.runelite.api.NPC;
|
||||||
|
import net.runelite.api.Player;
|
||||||
|
import net.runelite.api.events.AnimationChanged;
|
||||||
|
import net.runelite.api.events.ChatMessage;
|
||||||
|
import net.runelite.api.events.FakeXpDrop;
|
||||||
|
import net.runelite.api.events.GameStateChanged;
|
||||||
|
import net.runelite.api.events.GameTick;
|
||||||
|
import net.runelite.api.events.HitsplatApplied;
|
||||||
|
import net.runelite.api.events.InteractingChanged;
|
||||||
|
import net.runelite.api.events.NpcDespawned;
|
||||||
|
import net.runelite.api.events.NpcSpawned;
|
||||||
|
import net.runelite.api.events.SoundEffectPlayed;
|
||||||
|
import net.runelite.api.events.StatChanged;
|
||||||
|
import net.runelite.api.events.VarbitChanged;
|
||||||
|
import net.runelite.api.gameval.VarPlayerID;
|
||||||
|
import net.runelite.api.gameval.VarbitID;
|
||||||
|
import net.runelite.client.eventbus.Subscribe;
|
||||||
|
import net.runelite.client.game.ItemManager;
|
||||||
|
import net.runelite.client.game.NPCManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deterministic attack timer for the local player, incorporating weapon speeds, attack styles,
|
||||||
|
* manual casting, powered staves, boss mechanics, and food delays.
|
||||||
|
* NPC attack speeds are learned from intervals between their verified attack animations.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Singleton
|
||||||
|
public class AttackTimers
|
||||||
|
{
|
||||||
|
public enum AttackState
|
||||||
|
{
|
||||||
|
NOT_ATTACKING,
|
||||||
|
DELAYED_FIRST_TICK,
|
||||||
|
DELAYED
|
||||||
|
}
|
||||||
|
|
||||||
|
static final int DEFAULT_SPEED = 4;
|
||||||
|
private static final int MAX_SPEED = 10;
|
||||||
|
private static final int TRUSTED_SAMPLES = 3;
|
||||||
|
private static final long TICK_NANOS = 600_000_000L;
|
||||||
|
private static final int CONFIRM_TICKS = 4;
|
||||||
|
private static final int UI_HIDE_DEBOUNCE_TICKS_MAX = 1;
|
||||||
|
|
||||||
|
private static final int SLOW_FOOD_ATTACK_DELAY_TICKS = 4;
|
||||||
|
private static final int DEFAULT_FOOD_ATTACK_DELAY_TICKS = 3;
|
||||||
|
private static final int FAST_EAT_ATTACK_DELAY_TICKS = 2;
|
||||||
|
|
||||||
|
private static final String GENERIC_EAT = "You eat";
|
||||||
|
private static final String VAMPYRIUM_EAT = "Your stomach doesn't like it... but it heals some health";
|
||||||
|
private static final String BARBARIAN_POTIONS = "You drink the lumpy potion";
|
||||||
|
private static final String JUG_OF_WINE = "You drink the wine";
|
||||||
|
|
||||||
|
private static final Pattern EAT_MESSAGE = Pattern.compile(
|
||||||
|
"^(" + GENERIC_EAT + "|" + BARBARIAN_POTIONS + "|" + JUG_OF_WINE + "|" + VAMPYRIUM_EAT + ")",
|
||||||
|
Pattern.MULTILINE | Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
private static final Pattern SLOW_FOOD = Pattern.compile("^(" + VAMPYRIUM_EAT + ")",
|
||||||
|
Pattern.MULTILINE | Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
private static final String FAST_GNOME_FOOD = "worm hole|tangled toads legs|veg ball|chocolate bomb|worm crunchies|toad crunchies|"
|
||||||
|
+ "choc chip crunchies|spicy crunchies|fruit batta|cheese and tomato batta|toad batta|vegetable batta|worm batta";
|
||||||
|
private static final String FAST_FOOD = "karambwan|halibut";
|
||||||
|
private static final Pattern FAST_EAT = Pattern.compile("(" + FAST_FOOD + "|" + FAST_GNOME_FOOD + ")",
|
||||||
|
Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
private static final Set<Integer> NON_ATTACK_ANIMS = Set.of(829, 424, 1156, 836);
|
||||||
|
|
||||||
|
private final Client client;
|
||||||
|
private final ItemManager itemManager;
|
||||||
|
private final NPCManager npcManager;
|
||||||
|
private final EssentialsPlusConfig config;
|
||||||
|
|
||||||
|
private final TickCount tc = new TickCount();
|
||||||
|
private final VariableSpeed variableSpeed = new VariableSpeed(tc);
|
||||||
|
private final AttackSpeed attackSpeed = new AttackSpeed(tc, variableSpeed);
|
||||||
|
|
||||||
|
public AttackState attackState = AttackState.NOT_ATTACKING;
|
||||||
|
public AttackState renderedState = AttackState.NOT_ATTACKING;
|
||||||
|
|
||||||
|
public int tickPeriod = 0;
|
||||||
|
public int attackDelayHoldoffTicks = 0;
|
||||||
|
private int uiHideDebounceTickCount = 0;
|
||||||
|
public int pendingEatDelayTicks = 0;
|
||||||
|
|
||||||
|
private Spellbook currentSpellBook = Spellbook.STANDARD;
|
||||||
|
private int lastUsedWeaponId = -1;
|
||||||
|
private Actor lastTarget = null;
|
||||||
|
|
||||||
|
private volatile long lastTickNanos = System.nanoTime();
|
||||||
|
|
||||||
|
// NPC Attack Timers state
|
||||||
|
private final Map<NPC, Integer> npcAttackTicks = new HashMap<>();
|
||||||
|
private final Map<NPC, Integer> npcPrevTicks = new HashMap<>();
|
||||||
|
private final Map<Integer, SpeedSamples> npcSpeeds = new HashMap<>();
|
||||||
|
private final Map<NPC, int[]> pendingNpcAnims = new HashMap<>();
|
||||||
|
private final Map<Long, int[]> animOutcomes = new HashMap<>();
|
||||||
|
private final java.util.List<AnimCheck> animChecks = new java.util.ArrayList<>();
|
||||||
|
|
||||||
|
private static final class AnimCheck
|
||||||
|
{
|
||||||
|
final long key;
|
||||||
|
final int tick;
|
||||||
|
boolean confirmed;
|
||||||
|
|
||||||
|
AnimCheck(long key, int tick)
|
||||||
|
{
|
||||||
|
this.key = key;
|
||||||
|
this.tick = tick;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public AttackTimers(Client client, ItemManager itemManager, NPCManager npcManager, EssentialsPlusConfig config)
|
||||||
|
{
|
||||||
|
this.client = client;
|
||||||
|
this.itemManager = itemManager;
|
||||||
|
this.npcManager = npcManager;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Timer
|
||||||
|
{
|
||||||
|
public final int remaining;
|
||||||
|
public final int speed;
|
||||||
|
public final double tickProgress;
|
||||||
|
|
||||||
|
public Timer(int remaining, int speed, double tickProgress)
|
||||||
|
{
|
||||||
|
this.remaining = remaining;
|
||||||
|
this.speed = speed;
|
||||||
|
this.tickProgress = tickProgress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Timer playerTimer()
|
||||||
|
{
|
||||||
|
if (attackState == AttackState.NOT_ATTACKING && uiHideDebounceTickCount <= 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int remaining = 1 + attackDelayHoldoffTicks;
|
||||||
|
if (remaining < 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int speed = Math.max(1, tickPeriod);
|
||||||
|
double progress = (remaining == 0) ? 0 : tickProgress();
|
||||||
|
return new Timer(remaining, speed, progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double tickProgress()
|
||||||
|
{
|
||||||
|
return Math.min(1, Math.max(0, (System.nanoTime() - lastTickNanos) / (double) TICK_NANOS));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onRender()
|
||||||
|
{
|
||||||
|
final int delta = variableSpeed.shadowCrash.onRender(client, attackDelayHoldoffTicks, attackSpeed.isUsingMagic());
|
||||||
|
if (delta != 0)
|
||||||
|
{
|
||||||
|
attackDelayHoldoffTicks += delta;
|
||||||
|
if (attackDelayHoldoffTicks < 0)
|
||||||
|
{
|
||||||
|
attackState = AttackState.NOT_ATTACKING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (variableSpeed.maggotKing.onRender(client, itemManager)
|
||||||
|
|| variableSpeed.doomOfMokhaiotl.onRender(client, itemManager, currentSpellBook))
|
||||||
|
{
|
||||||
|
performAttack();
|
||||||
|
}
|
||||||
|
checkForLateWeaponSwaps();
|
||||||
|
renderedState = attackState;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setAttackDelay()
|
||||||
|
{
|
||||||
|
Player localPlayer = client.getLocalPlayer();
|
||||||
|
final AnimationData curAnimation = localPlayer != null ? AnimationData.fromId(localPlayer.getAnimation()) : null;
|
||||||
|
attackDelayHoldoffTicks = attackSpeed.compute(client, curAnimation, currentSpellBook, itemManager);
|
||||||
|
lastUsedWeaponId = CombatUtils.getWeaponId(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void performAttack()
|
||||||
|
{
|
||||||
|
attackState = AttackState.DELAYED_FIRST_TICK;
|
||||||
|
setAttackDelay();
|
||||||
|
tickPeriod = attackDelayHoldoffTicks;
|
||||||
|
uiHideDebounceTickCount = UI_HIDE_DEBOUNCE_TICKS_MAX;
|
||||||
|
Player localPlayer = client.getLocalPlayer();
|
||||||
|
lastTarget = localPlayer != null ? localPlayer.getInteracting() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkForLateWeaponSwaps()
|
||||||
|
{
|
||||||
|
final boolean weaponMismatch = CombatUtils.getWeaponId(client) != lastUsedWeaponId;
|
||||||
|
if (inPreAttackWindow() && weaponMismatch)
|
||||||
|
{
|
||||||
|
performAttack();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean inPreAttackWindow()
|
||||||
|
{
|
||||||
|
return attackState == AttackState.DELAYED_FIRST_TICK && renderedState != attackState;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyAndClearEats()
|
||||||
|
{
|
||||||
|
int pendingEats = pendingEatDelayTicks;
|
||||||
|
attackDelayHoldoffTicks += pendingEats;
|
||||||
|
pendingEatDelayTicks -= pendingEats;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onVarbitChanged(final VarbitChanged varbitChanged)
|
||||||
|
{
|
||||||
|
if (varbitChanged.getVarbitId() == VarbitID.SPELLBOOK)
|
||||||
|
{
|
||||||
|
currentSpellBook = Spellbook.fromVarbit(varbitChanged.getValue());
|
||||||
|
}
|
||||||
|
if (varbitChanged.getVarpId() == VarPlayerID.SA_ENERGY)
|
||||||
|
{
|
||||||
|
attackSpeed.varbitSpecialAttackChanged(varbitChanged.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onSoundEffectPlayed(final SoundEffectPlayed event)
|
||||||
|
{
|
||||||
|
attackSpeed.onSoundEffectPlayed(client, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onFakeXpDrop(final FakeXpDrop event)
|
||||||
|
{
|
||||||
|
if (attackSpeed.onXpDrop(event) && inPreAttackWindow())
|
||||||
|
{
|
||||||
|
performAttack();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onStatChanged(final StatChanged event)
|
||||||
|
{
|
||||||
|
if (attackSpeed.onXpDrop(event) && inPreAttackWindow())
|
||||||
|
{
|
||||||
|
performAttack();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onNpcSpawned(final NpcSpawned npcSpawned)
|
||||||
|
{
|
||||||
|
variableSpeed.onNpcSpawned(client, npcSpawned);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onNpcDespawned(final NpcDespawned npcDespawned)
|
||||||
|
{
|
||||||
|
variableSpeed.onNpcDespawned(client, npcDespawned);
|
||||||
|
npcAttackTicks.remove(npcDespawned.getNpc());
|
||||||
|
npcPrevTicks.remove(npcDespawned.getNpc());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onChatMessage(final ChatMessage event)
|
||||||
|
{
|
||||||
|
final String message = event.getMessage();
|
||||||
|
if (message != null && EAT_MESSAGE.matcher(message).find())
|
||||||
|
{
|
||||||
|
int delay;
|
||||||
|
if (FAST_EAT.matcher(message).find())
|
||||||
|
{
|
||||||
|
delay = FAST_EAT_ATTACK_DELAY_TICKS;
|
||||||
|
}
|
||||||
|
else if (SLOW_FOOD.matcher(message).find())
|
||||||
|
{
|
||||||
|
delay = SLOW_FOOD_ATTACK_DELAY_TICKS;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
delay = DEFAULT_FOOD_ATTACK_DELAY_TICKS;
|
||||||
|
}
|
||||||
|
pendingEatDelayTicks += delay;
|
||||||
|
}
|
||||||
|
variableSpeed.onChatMessage(client, event);
|
||||||
|
if (message != null)
|
||||||
|
{
|
||||||
|
pendingEatDelayTicks += variableSpeed.maggotKing.onChatMessage(client, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onInteractingChanged(InteractingChanged interactingChanged)
|
||||||
|
{
|
||||||
|
Actor source = interactingChanged.getSource();
|
||||||
|
Actor target = interactingChanged.getTarget();
|
||||||
|
Player p = client.getLocalPlayer();
|
||||||
|
|
||||||
|
if (source != null && source.equals(p) && target instanceof NPC)
|
||||||
|
{
|
||||||
|
if (attackState == AttackState.NOT_ATTACKING && Attacking.isPlayerAttacking(client, npcManager))
|
||||||
|
{
|
||||||
|
performAttack();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
applyAndClearEats();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onGameTick(GameTick tick)
|
||||||
|
{
|
||||||
|
lastTickNanos = System.nanoTime();
|
||||||
|
variableSpeed.onGameTick(client, tick);
|
||||||
|
final boolean isAttacking = Attacking.isPlayerAttacking(client, npcManager);
|
||||||
|
|
||||||
|
switch (attackState)
|
||||||
|
{
|
||||||
|
case NOT_ATTACKING:
|
||||||
|
if (isAttacking)
|
||||||
|
{
|
||||||
|
performAttack();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
uiHideDebounceTickCount = Math.max(-20, uiHideDebounceTickCount - 1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case DELAYED_FIRST_TICK:
|
||||||
|
attackState = AttackState.DELAYED;
|
||||||
|
// fallthrough
|
||||||
|
case DELAYED:
|
||||||
|
if (attackDelayHoldoffTicks <= 0)
|
||||||
|
{
|
||||||
|
if (isAttacking)
|
||||||
|
{
|
||||||
|
performAttack();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
attackState = AttackState.NOT_ATTACKING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyAndClearEats();
|
||||||
|
attackDelayHoldoffTicks = Math.max(-20, attackDelayHoldoffTicks - 1);
|
||||||
|
attackSpeed.onTick();
|
||||||
|
|
||||||
|
// Update NPC attack timer learning
|
||||||
|
updateNpcTimers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateNpcTimers()
|
||||||
|
{
|
||||||
|
int tick = client.getTickCount();
|
||||||
|
animChecks.removeIf(c ->
|
||||||
|
{
|
||||||
|
if (!c.confirmed && tick - c.tick <= CONFIRM_TICKS)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
animOutcomes.computeIfAbsent(c.key, k -> new int[2])[c.confirmed ? 0 : 1]++;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
for (Map.Entry<NPC, int[]> pending : pendingNpcAnims.entrySet())
|
||||||
|
{
|
||||||
|
NPC npc = pending.getKey();
|
||||||
|
int animTick = pending.getValue()[0];
|
||||||
|
long animKey = (long) npc.getId() << 32 | pending.getValue()[1];
|
||||||
|
animChecks.add(new AnimCheck(animKey, animTick));
|
||||||
|
int[] outcome = animOutcomes.get(animKey);
|
||||||
|
if (outcome != null && outcome[1] >= 2 && outcome[1] > outcome[0] * 2)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
SpeedSamples samples = npcSpeeds.computeIfAbsent(npc.getId(), id -> new SpeedSamples());
|
||||||
|
Integer last = npcAttackTicks.get(npc);
|
||||||
|
if (last != null)
|
||||||
|
{
|
||||||
|
int gap = animTick - last;
|
||||||
|
if (gap <= 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (samples.count() >= TRUSTED_SAMPLES && gap < samples.speed())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
samples.add(gap);
|
||||||
|
npcPrevTicks.put(npc, last);
|
||||||
|
}
|
||||||
|
npcAttackTicks.put(npc, animTick);
|
||||||
|
}
|
||||||
|
pendingNpcAnims.clear();
|
||||||
|
npcAttackTicks.values().removeIf(t -> tick - t > MAX_SPEED + 2);
|
||||||
|
npcPrevTicks.keySet().retainAll(npcAttackTicks.keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onAnimationChanged(AnimationChanged event)
|
||||||
|
{
|
||||||
|
if (!config.enemyAttackTimer())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Actor actor = event.getActor();
|
||||||
|
int anim = actor.getAnimation();
|
||||||
|
if (anim == -1 || NON_ATTACK_ANIMS.contains(anim))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Player me = client.getLocalPlayer();
|
||||||
|
if (actor instanceof NPC && me != null && actor.getInteracting() == me && !actor.isDead())
|
||||||
|
{
|
||||||
|
pendingNpcAnims.putIfAbsent((NPC) actor, new int[]{client.getTickCount(), anim});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onHitsplatApplied(HitsplatApplied event)
|
||||||
|
{
|
||||||
|
if (event.getActor() == client.getLocalPlayer() && !event.getHitsplat().isMine())
|
||||||
|
{
|
||||||
|
for (AnimCheck c : animChecks)
|
||||||
|
{
|
||||||
|
if (!c.confirmed)
|
||||||
|
{
|
||||||
|
c.confirmed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onGameStateChanged(GameStateChanged event)
|
||||||
|
{
|
||||||
|
if (event.getGameState() != GameState.LOGGED_IN)
|
||||||
|
{
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset()
|
||||||
|
{
|
||||||
|
attackState = AttackState.NOT_ATTACKING;
|
||||||
|
renderedState = AttackState.NOT_ATTACKING;
|
||||||
|
tickPeriod = 0;
|
||||||
|
attackDelayHoldoffTicks = 0;
|
||||||
|
uiHideDebounceTickCount = 0;
|
||||||
|
pendingEatDelayTicks = 0;
|
||||||
|
lastUsedWeaponId = -1;
|
||||||
|
lastTarget = null;
|
||||||
|
tc.reset();
|
||||||
|
attackSpeed.reset();
|
||||||
|
npcAttackTicks.clear();
|
||||||
|
npcPrevTicks.clear();
|
||||||
|
pendingNpcAnims.clear();
|
||||||
|
animChecks.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<NPC, Timer> npcTimers()
|
||||||
|
{
|
||||||
|
Map<NPC, Timer> out = new HashMap<>();
|
||||||
|
Player me = client.getLocalPlayer();
|
||||||
|
for (Map.Entry<NPC, Integer> e : npcAttackTicks.entrySet())
|
||||||
|
{
|
||||||
|
NPC npc = e.getKey();
|
||||||
|
if (npc.isDead() || me == null || npc.getInteracting() != me)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int attackTick = e.getValue();
|
||||||
|
if (attackTick >= client.getTickCount())
|
||||||
|
{
|
||||||
|
Integer prev = npcPrevTicks.get(npc);
|
||||||
|
attackTick = prev == null ? -1 : prev;
|
||||||
|
}
|
||||||
|
Timer t = npcTimer(attackTick, npcSpeed(npc.getId()));
|
||||||
|
if (t != null)
|
||||||
|
{
|
||||||
|
out.put(npc, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Timer npcTimer(int attackTick, int speed)
|
||||||
|
{
|
||||||
|
if (attackTick < 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int remaining = speed - (client.getTickCount() - attackTick - 1);
|
||||||
|
if (remaining < 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new Timer(remaining, speed, remaining == 0 ? 0 : tickProgress());
|
||||||
|
}
|
||||||
|
|
||||||
|
int npcSpeed(int npcId)
|
||||||
|
{
|
||||||
|
SpeedSamples s = npcSpeeds.get(npcId);
|
||||||
|
return s == null ? DEFAULT_SPEED : s.speed();
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class SpeedSamples
|
||||||
|
{
|
||||||
|
private final int[] counts = new int[MAX_SPEED + 1];
|
||||||
|
private int total;
|
||||||
|
|
||||||
|
void add(int gap)
|
||||||
|
{
|
||||||
|
if (gap >= 1 && gap <= MAX_SPEED)
|
||||||
|
{
|
||||||
|
counts[gap]++;
|
||||||
|
total++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int count()
|
||||||
|
{
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
int speed()
|
||||||
|
{
|
||||||
|
int best = DEFAULT_SPEED;
|
||||||
|
int bestCount = 0;
|
||||||
|
for (int gap = 1; gap <= MAX_SPEED; gap++)
|
||||||
|
{
|
||||||
|
if (counts[gap] > bestCount)
|
||||||
|
{
|
||||||
|
best = gap;
|
||||||
|
bestCount = counts[gap];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.example.essentialsplus.helpers;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import net.runelite.api.gameval.SpriteID;
|
||||||
|
|
||||||
|
/** Attack style of an NPC attack, with the protection prayer that blocks it. */
|
||||||
|
public enum CombatStyle
|
||||||
|
{
|
||||||
|
MELEE("Melee", new Color(0xE05050), SpriteID.Prayeron.PROTECT_FROM_MELEE),
|
||||||
|
RANGED("Range", new Color(0x6BD66B), SpriteID.Prayeron.PROTECT_FROM_MISSILES),
|
||||||
|
MAGIC("Mage", new Color(0x5AA9FF), SpriteID.Prayeron.PROTECT_FROM_MAGIC);
|
||||||
|
|
||||||
|
public final String label;
|
||||||
|
public final Color color;
|
||||||
|
public final int prayerSprite;
|
||||||
|
|
||||||
|
CombatStyle(String label, Color color, int prayerSprite)
|
||||||
|
{
|
||||||
|
this.label = label;
|
||||||
|
this.color = color;
|
||||||
|
this.prayerSprite = prayerSprite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Style from a cache name such as {@code HUNLLEF_ATTACK_RANGED} or {@code ZEBAK_MAGE_PROJANIM_INITIAL};
|
||||||
|
* null when the name mentions no style or more than one.
|
||||||
|
*/
|
||||||
|
static CombatStyle fromName(String name)
|
||||||
|
{
|
||||||
|
CombatStyle found = null;
|
||||||
|
for (String token : name.toUpperCase(java.util.Locale.ROOT).split("_"))
|
||||||
|
{
|
||||||
|
CombatStyle s = fromToken(token);
|
||||||
|
if (s == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (found != null && found != s)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
found = s;
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CombatStyle fromToken(String token)
|
||||||
|
{
|
||||||
|
if (token.startsWith("MELEE"))
|
||||||
|
{
|
||||||
|
return MELEE;
|
||||||
|
}
|
||||||
|
if (token.startsWith("RANGE") || token.startsWith("ARROW") || token.startsWith("DART") || token.startsWith("JAVELIN"))
|
||||||
|
{
|
||||||
|
return RANGED;
|
||||||
|
}
|
||||||
|
if (token.startsWith("MAGIC") || token.startsWith("MAGE") && !token.startsWith("MAGENTA"))
|
||||||
|
{
|
||||||
|
return MAGIC;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,105 @@
|
|||||||
|
package com.example.essentialsplus.helpers;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.EssentialsPlusConfig;
|
||||||
|
import com.example.essentialsplus.EssentialsPlusPlugin;
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.Font;
|
||||||
|
import java.awt.FontMetrics;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.Rectangle;
|
||||||
|
import java.awt.RenderingHints;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import net.runelite.api.widgets.WidgetItem;
|
||||||
|
import net.runelite.client.ui.FontManager;
|
||||||
|
import net.runelite.client.ui.overlay.OverlayLayer;
|
||||||
|
import net.runelite.client.ui.overlay.OverlayPosition;
|
||||||
|
import net.runelite.client.ui.overlay.WidgetItemOverlay;
|
||||||
|
import net.runelite.client.ui.overlay.components.TextComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overlay displaying charge counts / statuses on item icons in Inventory, Equipment and (optionally) Bank.
|
||||||
|
*/
|
||||||
|
public class ItemChargesOverlay extends WidgetItemOverlay
|
||||||
|
{
|
||||||
|
private final EssentialsPlusConfig config;
|
||||||
|
private final EssentialsPlusPlugin plugin;
|
||||||
|
private final ItemCharges itemCharges;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ItemChargesOverlay(EssentialsPlusConfig config, EssentialsPlusPlugin plugin, ItemCharges itemCharges)
|
||||||
|
{
|
||||||
|
this.config = config;
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.itemCharges = itemCharges;
|
||||||
|
|
||||||
|
showOnInventory();
|
||||||
|
showOnEquipment();
|
||||||
|
showOnBank();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void renderItemOverlay(Graphics2D g, int itemId, WidgetItem widgetItem)
|
||||||
|
{
|
||||||
|
if (!config.itemCharges())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if on bank and if bank display is allowed
|
||||||
|
boolean showBank = plugin.getHelperValue("itemChargesBank", Boolean.class, true);
|
||||||
|
if (!showBank && widgetItem.getWidget() != null)
|
||||||
|
{
|
||||||
|
int group = net.runelite.api.widgets.WidgetUtil.componentToInterface(widgetItem.getWidget().getId());
|
||||||
|
if (group == net.runelite.api.gameval.InterfaceID.BANKMAIN || group == 12) // 12 = InterfaceID.BANK
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ItemCharges.ChargeValue> charges = itemCharges.chargesFor(itemId);
|
||||||
|
if (charges == null || charges.isEmpty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||||
|
|
||||||
|
TimerTextSize size = plugin.getHelperValue("itemChargesSize", TimerTextSize.class, TimerTextSize.SMALL);
|
||||||
|
Font font = (size == TimerTextSize.SMALL) ? FontManager.getRunescapeSmallFont() : size.font();
|
||||||
|
g.setFont(font);
|
||||||
|
|
||||||
|
Rectangle bounds = widgetItem.getCanvasBounds();
|
||||||
|
if (bounds == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single charge or Top charge (top-left: bounds.x - 1, bounds.y + 11)
|
||||||
|
ItemCharges.ChargeValue primary = charges.get(0);
|
||||||
|
if (primary != null && primary.getText() != null && !primary.getText().isEmpty())
|
||||||
|
{
|
||||||
|
TextComponent textComponent = new TextComponent();
|
||||||
|
textComponent.setFont(font);
|
||||||
|
textComponent.setColor(primary.getColor());
|
||||||
|
textComponent.setText(primary.getText());
|
||||||
|
textComponent.setPosition(new java.awt.Point(bounds.x - 1, bounds.y + 11));
|
||||||
|
textComponent.render(g);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secondary charge (bottom-left: bounds.x - 1, bounds.y + bounds.height - 2)
|
||||||
|
if (charges.size() > 1)
|
||||||
|
{
|
||||||
|
ItemCharges.ChargeValue secondary = charges.get(1);
|
||||||
|
if (secondary != null && secondary.getText() != null && !secondary.getText().isEmpty())
|
||||||
|
{
|
||||||
|
TextComponent secondaryComponent = new TextComponent();
|
||||||
|
secondaryComponent.setFont(font);
|
||||||
|
secondaryComponent.setColor(secondary.getColor());
|
||||||
|
secondaryComponent.setText(secondary.getText());
|
||||||
|
secondaryComponent.setPosition(new java.awt.Point(bounds.x - 1, bounds.y + bounds.height - 2));
|
||||||
|
secondaryComponent.render(g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package com.example.essentialsplus.helpers;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.EssentialsPlusConfig;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.ItemComposition;
|
||||||
|
import net.runelite.api.MenuEntry;
|
||||||
|
import net.runelite.api.gameval.ItemID;
|
||||||
|
import net.runelite.api.widgets.Widget;
|
||||||
|
import net.runelite.api.widgets.WidgetUtil;
|
||||||
|
import net.runelite.client.game.ItemManager;
|
||||||
|
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.ColorUtil;
|
||||||
|
import net.runelite.client.util.QuantityFormatter;
|
||||||
|
|
||||||
|
/** Tooltip line with the GE and/or high alch value of the inventory item under the mouse. */
|
||||||
|
public class ItemPriceOverlay extends Overlay
|
||||||
|
{
|
||||||
|
private static final int INVENTORY = 149;
|
||||||
|
private static final int BANK_INVENTORY = 15;
|
||||||
|
private static final Color GE = new Color(0xFFDC33);
|
||||||
|
private static final Color HA = new Color(0xFF9040);
|
||||||
|
|
||||||
|
private final Client client;
|
||||||
|
private final EssentialsPlusConfig config;
|
||||||
|
private final ItemManager itemManager;
|
||||||
|
private final TooltipManager tooltipManager;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ItemPriceOverlay(Client client, EssentialsPlusConfig config, ItemManager itemManager, TooltipManager tooltipManager)
|
||||||
|
{
|
||||||
|
this.client = client;
|
||||||
|
this.config = config;
|
||||||
|
this.itemManager = itemManager;
|
||||||
|
this.tooltipManager = tooltipManager;
|
||||||
|
setPosition(OverlayPosition.DYNAMIC);
|
||||||
|
setLayer(OverlayLayer.ABOVE_WIDGETS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension render(Graphics2D g)
|
||||||
|
{
|
||||||
|
if (!config.itemPrices() || (!config.itemPricesGe() && !config.itemPricesHa()) || client.isMenuOpen())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
MenuEntry[] entries = client.getMenuEntries();
|
||||||
|
if (entries.length == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Widget w = entries[entries.length - 1].getWidget();
|
||||||
|
if (w == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int group = WidgetUtil.componentToInterface(w.getId());
|
||||||
|
if (group != INVENTORY && group != BANK_INVENTORY)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int id = w.getItemId();
|
||||||
|
if (id <= 0 || id == ItemID.COINS || id == ItemID.PLATINUM)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
long qty = Math.max(1, w.getItemQuantity());
|
||||||
|
String text = text(itemManager.canonicalize(id), qty);
|
||||||
|
if (!text.isEmpty())
|
||||||
|
{
|
||||||
|
tooltipManager.add(new Tooltip(text));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String text(int id, long qty)
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
if (config.itemPricesGe())
|
||||||
|
{
|
||||||
|
long ge = (long) itemManager.getItemPrice(id) * qty;
|
||||||
|
if (ge > 0)
|
||||||
|
{
|
||||||
|
sb.append("GE: ").append(ColorUtil.wrapWithColorTag(gp(ge), GE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (config.itemPricesHa())
|
||||||
|
{
|
||||||
|
ItemComposition comp = itemManager.getItemComposition(id);
|
||||||
|
long ha = comp == null ? 0 : (long) comp.getHaPrice() * qty;
|
||||||
|
if (ha > 0)
|
||||||
|
{
|
||||||
|
if (sb.length() > 0)
|
||||||
|
{
|
||||||
|
sb.append(" ");
|
||||||
|
}
|
||||||
|
sb.append("HA: ").append(ColorUtil.wrapWithColorTag(gp(ha), HA));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String gp(long v)
|
||||||
|
{
|
||||||
|
return QuantityFormatter.quantityToStackSize(v) + " gp";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.example.essentialsplus.helpers;
|
||||||
|
|
||||||
|
import java.awt.Font;
|
||||||
|
import net.runelite.client.ui.FontManager;
|
||||||
|
|
||||||
|
/** Font of the attack timer numbers. */
|
||||||
|
public enum TimerTextSize
|
||||||
|
{
|
||||||
|
SMALL("Small"),
|
||||||
|
NORMAL("Normal"),
|
||||||
|
LARGE("Large");
|
||||||
|
|
||||||
|
private final String label;
|
||||||
|
|
||||||
|
TimerTextSize(String label)
|
||||||
|
{
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Font font()
|
||||||
|
{
|
||||||
|
switch (this)
|
||||||
|
{
|
||||||
|
case SMALL:
|
||||||
|
return FontManager.getRunescapeSmallFont();
|
||||||
|
case LARGE:
|
||||||
|
return FontManager.getRunescapeBoldFont().deriveFont(24f);
|
||||||
|
default:
|
||||||
|
return FontManager.getRunescapeBoldFont();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Icon edge length in pixels. */
|
||||||
|
public int iconSize()
|
||||||
|
{
|
||||||
|
switch (this)
|
||||||
|
{
|
||||||
|
case SMALL:
|
||||||
|
return 16;
|
||||||
|
case LARGE:
|
||||||
|
return 32;
|
||||||
|
default:
|
||||||
|
return 22;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
package com.example.essentialsplus.helpers;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.EssentialsPlusConfig;
|
||||||
|
import com.example.essentialsplus.EssentialsPlusPlugin;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.FontMetrics;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.RenderingHints;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.Perspective;
|
||||||
|
import net.runelite.api.Point;
|
||||||
|
import net.runelite.api.coords.LocalPoint;
|
||||||
|
import net.runelite.client.ui.overlay.Overlay;
|
||||||
|
import net.runelite.client.ui.overlay.OverlayLayer;
|
||||||
|
import net.runelite.client.ui.overlay.OverlayPosition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overlay displaying:
|
||||||
|
* - Remaining seconds until stump respawn
|
||||||
|
* - Remaining chop duration on high tier trees (Maple+)
|
||||||
|
* - Optional progress bar and customizable styling
|
||||||
|
*/
|
||||||
|
public class TreeCooldownOverlay extends Overlay
|
||||||
|
{
|
||||||
|
private static final Color BAR_BG = new Color(0, 0, 0, 160);
|
||||||
|
private static final Color BAR_BORDER = new Color(0, 0, 0, 220);
|
||||||
|
private static final int BAR_WIDTH = 32;
|
||||||
|
private static final int BAR_HEIGHT = 4;
|
||||||
|
|
||||||
|
private final Client client;
|
||||||
|
private final EssentialsPlusConfig config;
|
||||||
|
private final EssentialsPlusPlugin plugin;
|
||||||
|
private final TreeCooldowns treeCooldowns;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public TreeCooldownOverlay(Client client, EssentialsPlusConfig config, EssentialsPlusPlugin plugin, TreeCooldowns treeCooldowns)
|
||||||
|
{
|
||||||
|
this.client = client;
|
||||||
|
this.config = config;
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.treeCooldowns = treeCooldowns;
|
||||||
|
setPosition(OverlayPosition.DYNAMIC);
|
||||||
|
setLayer(OverlayLayer.ABOVE_SCENE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension render(Graphics2D g)
|
||||||
|
{
|
||||||
|
if (!config.treeCooldowns())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
|
||||||
|
boolean showRespawn = plugin.getHelperValue("treeCooldownRespawn", Boolean.class, true);
|
||||||
|
boolean showChop = plugin.getHelperValue("treeCooldownChop", Boolean.class, true);
|
||||||
|
boolean showBar = plugin.getHelperValue("treeCooldownBar", Boolean.class, false);
|
||||||
|
TimerTextSize size = plugin.getHelperValue("treeCooldownSize", TimerTextSize.class, TimerTextSize.NORMAL);
|
||||||
|
int offset = plugin.getHelperValue("treeCooldownOffset", Integer.class, 50);
|
||||||
|
Color textColor = plugin.getHelperValue("treeCooldownColor", Color.class, new Color(0x8FBF3F));
|
||||||
|
Color readyColor = plugin.getHelperValue("treeCooldownReadyColor", Color.class, new Color(0x66FF66));
|
||||||
|
|
||||||
|
int plane = client.getPlane();
|
||||||
|
|
||||||
|
if (showRespawn)
|
||||||
|
{
|
||||||
|
for (TreeCooldowns.RespawnTimer timer : treeCooldowns.getActiveRespawnTimers())
|
||||||
|
{
|
||||||
|
if (timer.worldPoint.getPlane() != plane)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sec = timer.getRemainingSeconds();
|
||||||
|
if (sec <= 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Point anchor = null;
|
||||||
|
if (timer.stumpObject != null)
|
||||||
|
{
|
||||||
|
anchor = timer.stumpObject.getCanvasTextLocation(g, "", offset);
|
||||||
|
}
|
||||||
|
if (anchor == null)
|
||||||
|
{
|
||||||
|
LocalPoint lp = LocalPoint.fromWorld(client, timer.worldPoint);
|
||||||
|
if (lp != null)
|
||||||
|
{
|
||||||
|
anchor = Perspective.getCanvasTextLocation(client, g, lp, "", offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (anchor == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color c = (sec <= 2) ? readyColor : textColor;
|
||||||
|
drawTimer(g, anchor, sec, c, size, showBar, timer.getProgress());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showChop)
|
||||||
|
{
|
||||||
|
for (TreeCooldowns.ChopTimer timer : treeCooldowns.getActiveChopTimers())
|
||||||
|
{
|
||||||
|
if (timer.worldPoint.getPlane() != plane)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sec = timer.getRemainingSeconds();
|
||||||
|
if (sec <= 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Point anchor = null;
|
||||||
|
if (timer.treeObject != null)
|
||||||
|
{
|
||||||
|
anchor = timer.treeObject.getCanvasTextLocation(g, "", offset + 80);
|
||||||
|
}
|
||||||
|
if (anchor == null)
|
||||||
|
{
|
||||||
|
LocalPoint lp = LocalPoint.fromWorld(client, timer.worldPoint);
|
||||||
|
if (lp != null)
|
||||||
|
{
|
||||||
|
anchor = Perspective.getCanvasTextLocation(client, g, lp, "", offset + 80);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (anchor == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color c = (sec <= 5) ? readyColor : textColor;
|
||||||
|
drawTimer(g, anchor, sec, c, size, showBar, timer.getProgress());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawTimer(Graphics2D g, Point anchor, int seconds, Color color, TimerTextSize size, boolean showBar, double progress)
|
||||||
|
{
|
||||||
|
g.setFont(size.font());
|
||||||
|
String text = String.valueOf(seconds);
|
||||||
|
FontMetrics fm = g.getFontMetrics();
|
||||||
|
int textX = anchor.getX() - fm.stringWidth(text) / 2;
|
||||||
|
int textY = anchor.getY();
|
||||||
|
|
||||||
|
shadowed(g, text, textX, textY, color);
|
||||||
|
|
||||||
|
if (showBar)
|
||||||
|
{
|
||||||
|
int barX = anchor.getX() - BAR_WIDTH / 2;
|
||||||
|
int barY = textY + 4;
|
||||||
|
|
||||||
|
g.setColor(BAR_BG);
|
||||||
|
g.fillRect(barX, barY, BAR_WIDTH, BAR_HEIGHT);
|
||||||
|
|
||||||
|
int fill = (int) Math.round(BAR_WIDTH * Math.max(0.0, Math.min(1.0, progress)));
|
||||||
|
if (fill > 0)
|
||||||
|
{
|
||||||
|
g.setColor(color);
|
||||||
|
g.fillRect(barX, barY, fill, BAR_HEIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
g.setColor(BAR_BORDER);
|
||||||
|
g.drawRect(barX, barY, BAR_WIDTH, BAR_HEIGHT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void shadowed(Graphics2D g, String text, int x, int y, Color color)
|
||||||
|
{
|
||||||
|
g.setColor(Color.BLACK);
|
||||||
|
g.drawString(text, x + 1, y + 1);
|
||||||
|
g.setColor(color);
|
||||||
|
g.drawString(text, x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,790 @@
|
|||||||
|
package com.example.essentialsplus.helpers;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.EssentialsPlusConfig;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.GameObject;
|
||||||
|
import net.runelite.api.GameState;
|
||||||
|
import net.runelite.api.ObjectComposition;
|
||||||
|
import net.runelite.api.Perspective;
|
||||||
|
import net.runelite.api.Player;
|
||||||
|
import net.runelite.api.ScriptID;
|
||||||
|
import net.runelite.api.Tile;
|
||||||
|
import net.runelite.api.coords.LocalPoint;
|
||||||
|
import net.runelite.api.coords.WorldPoint;
|
||||||
|
import net.runelite.api.events.GameObjectDespawned;
|
||||||
|
import net.runelite.api.events.GameObjectSpawned;
|
||||||
|
import net.runelite.api.events.GameStateChanged;
|
||||||
|
import net.runelite.api.events.GameTick;
|
||||||
|
import net.runelite.api.events.ScriptPreFired;
|
||||||
|
import net.runelite.api.gameval.AnimationID;
|
||||||
|
import net.runelite.api.gameval.ObjectID;
|
||||||
|
import net.runelite.client.eventbus.Subscribe;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks tree cooldowns:
|
||||||
|
* 1. Remaining chop duration for long-lived trees (Maple+, despawnSec > 30) with regeneration when idle.
|
||||||
|
* 2. Respawn timers on stumps felled nearby (exact tick synchronization via game script and object events).
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
public class TreeCooldowns
|
||||||
|
{
|
||||||
|
/** Woodcutting animations for standard, gilded, infernal, crystal, 3rd age, trailblazer, and forestry 2H axes. */
|
||||||
|
public static final Set<Integer> WOODCUT_ANIMS = Set.of(
|
||||||
|
AnimationID.HUMAN_WOODCUTTING_BRONZE_AXE,
|
||||||
|
AnimationID.HUMAN_WOODCUTTING_IRON_AXE,
|
||||||
|
AnimationID.HUMAN_WOODCUTTING_STEEL_AXE,
|
||||||
|
AnimationID.HUMAN_WOODCUTTING_BLACK_AXE,
|
||||||
|
AnimationID.HUMAN_WOODCUTTING_MITHRIL_AXE,
|
||||||
|
AnimationID.HUMAN_WOODCUTTING_ADAMANT_AXE,
|
||||||
|
AnimationID.HUMAN_WOODCUTTING_RUNE_AXE,
|
||||||
|
AnimationID.HUMAN_WOODCUTTING_GILDED_AXE,
|
||||||
|
AnimationID.HUMAN_WOODCUTTING_DRAGON_AXE,
|
||||||
|
AnimationID.HUMAN_WOODCUTTING_TRAILBLAZER_AXE_NO_INFERNAL,
|
||||||
|
AnimationID.HUMAN_WOODCUTTING_INFERNAL_AXE,
|
||||||
|
AnimationID.HUMAN_WOODCUTTING_3A_AXE,
|
||||||
|
AnimationID.HUMAN_WOODCUTTING_CRYSTAL_AXE,
|
||||||
|
AnimationID.HUMAN_WOODCUTTING_TRAILBLAZER_RELOADED_AXE_NO_INFERNAL,
|
||||||
|
AnimationID.HUMAN_WOODCUTTING_TRAILBLAZER_AXE,
|
||||||
|
AnimationID.HUMAN_WOODCUTTING_TRAILBLAZER_RELOADED_AXE,
|
||||||
|
AnimationID.FORESTRY_2H_AXE_CHOPPING_BRONZE,
|
||||||
|
AnimationID.FORESTRY_2H_AXE_CHOPPING_IRON,
|
||||||
|
AnimationID.FORESTRY_2H_AXE_CHOPPING_STEEL,
|
||||||
|
AnimationID.FORESTRY_2H_AXE_CHOPPING_BLACK,
|
||||||
|
AnimationID.FORESTRY_2H_AXE_CHOPPING_MITHRIL,
|
||||||
|
AnimationID.FORESTRY_2H_AXE_CHOPPING_ADAMANT,
|
||||||
|
AnimationID.FORESTRY_2H_AXE_CHOPPING_RUNE,
|
||||||
|
AnimationID.FORESTRY_2H_AXE_CHOPPING_DRAGON,
|
||||||
|
AnimationID.FORESTRY_2H_AXE_CHOPPING_CRYSTAL,
|
||||||
|
AnimationID.FORESTRY_2H_AXE_CHOPPING_CRYSTAL_INACTIVE,
|
||||||
|
AnimationID.FORESTRY_2H_AXE_CHOPPING_3A
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Classification of tree species with their official despawn and respawn periods. */
|
||||||
|
public enum TreeType
|
||||||
|
{
|
||||||
|
OAK("Oak", 27, 8.4, 14),
|
||||||
|
WILLOW("Willow", 30, 8.4, 14),
|
||||||
|
TEAK("Teak", 30, 9.0, 15),
|
||||||
|
MAPLE("Maple", 60, 35.4, 59),
|
||||||
|
HOLLOW("Hollow", 36, 25.8, 43),
|
||||||
|
MAHOGANY("Mahogany", 60, 8.4, 14),
|
||||||
|
ARCTIC_PINE("Arctic pine", 84, 8.4, 14),
|
||||||
|
YEW("Yew", 114, 59.4, 99),
|
||||||
|
CAMPHOR("Camphor", 120, 59.4, 99),
|
||||||
|
MAGIC("Magic", 234, 119.4, 199),
|
||||||
|
IRONWOOD("Ironwood", 240, 119.4, 199),
|
||||||
|
REDWOOD("Redwood", 264, 119.4, 199),
|
||||||
|
ROSEWOOD("Rosewood", 275, 123.0, 205),
|
||||||
|
NORMAL("Tree", 0, 10.0, 16);
|
||||||
|
|
||||||
|
public final String displayName;
|
||||||
|
public final int despawnSec;
|
||||||
|
public final double respawnSec;
|
||||||
|
public final int respawnTicks;
|
||||||
|
|
||||||
|
TreeType(String displayName, int despawnSec, double respawnSec, int respawnTicks)
|
||||||
|
{
|
||||||
|
this.displayName = displayName;
|
||||||
|
this.despawnSec = despawnSec;
|
||||||
|
this.respawnSec = respawnSec;
|
||||||
|
this.respawnTicks = respawnTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDespawnTicks()
|
||||||
|
{
|
||||||
|
return (int) Math.round(despawnSec / 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasChopTimer()
|
||||||
|
{
|
||||||
|
return despawnSec > 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TreeType fromRespawnTicks(int ticks)
|
||||||
|
{
|
||||||
|
for (TreeType t : values())
|
||||||
|
{
|
||||||
|
if (t.respawnTicks == ticks)
|
||||||
|
{
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ticks <= 14) return OAK;
|
||||||
|
if (ticks <= 16) return NORMAL;
|
||||||
|
if (ticks <= 45) return HOLLOW;
|
||||||
|
if (ticks <= 65) return MAPLE;
|
||||||
|
if (ticks <= 105) return YEW;
|
||||||
|
if (ticks <= 150) return ROSEWOOD;
|
||||||
|
return MAGIC;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Information about a tree currently in scene. */
|
||||||
|
public static class TrackedTree
|
||||||
|
{
|
||||||
|
public final GameObject gameObject;
|
||||||
|
public final TreeType treeType;
|
||||||
|
public final WorldPoint worldPoint;
|
||||||
|
public final int sizeX;
|
||||||
|
public final int sizeY;
|
||||||
|
|
||||||
|
public int choppedTicks;
|
||||||
|
public int lastChoppedTick;
|
||||||
|
|
||||||
|
public TrackedTree(GameObject gameObject, TreeType treeType)
|
||||||
|
{
|
||||||
|
this.gameObject = gameObject;
|
||||||
|
this.treeType = treeType;
|
||||||
|
this.worldPoint = gameObject.getWorldLocation();
|
||||||
|
this.sizeX = gameObject.sizeX();
|
||||||
|
this.sizeY = gameObject.sizeY();
|
||||||
|
this.choppedTicks = 0;
|
||||||
|
this.lastChoppedTick = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Respawn timer for a felled tree stump. */
|
||||||
|
public static class RespawnTimer
|
||||||
|
{
|
||||||
|
public final TreeType treeType;
|
||||||
|
public final WorldPoint worldPoint;
|
||||||
|
public final long startTimeMs;
|
||||||
|
public final long deadlineMs;
|
||||||
|
public final int totalTicks;
|
||||||
|
public final int startTick;
|
||||||
|
public GameObject stumpObject;
|
||||||
|
|
||||||
|
public RespawnTimer(TreeType treeType, WorldPoint worldPoint, long startTimeMs, long deadlineMs, int totalTicks, int startTick)
|
||||||
|
{
|
||||||
|
this.treeType = treeType;
|
||||||
|
this.worldPoint = worldPoint;
|
||||||
|
this.startTimeMs = startTimeMs;
|
||||||
|
this.deadlineMs = deadlineMs;
|
||||||
|
this.totalTicks = totalTicks;
|
||||||
|
this.startTick = startTick;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRemainingSeconds()
|
||||||
|
{
|
||||||
|
long remaining = deadlineMs - System.currentTimeMillis();
|
||||||
|
return (int) Math.max(0, Math.ceil(remaining / 1000.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getProgress()
|
||||||
|
{
|
||||||
|
long total = deadlineMs - startTimeMs;
|
||||||
|
if (total <= 0)
|
||||||
|
{
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
long elapsed = System.currentTimeMillis() - startTimeMs;
|
||||||
|
return Math.max(0.0, Math.min(1.0, (double) elapsed / total));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Active chop timer for a tree being harvested. */
|
||||||
|
public static class ChopTimer
|
||||||
|
{
|
||||||
|
public final TreeType treeType;
|
||||||
|
public final WorldPoint worldPoint;
|
||||||
|
public final GameObject treeObject;
|
||||||
|
public final int remainingTicks;
|
||||||
|
public final int totalTicks;
|
||||||
|
|
||||||
|
public ChopTimer(TreeType treeType, WorldPoint worldPoint, GameObject treeObject, int remainingTicks, int totalTicks)
|
||||||
|
{
|
||||||
|
this.treeType = treeType;
|
||||||
|
this.worldPoint = worldPoint;
|
||||||
|
this.treeObject = treeObject;
|
||||||
|
this.remainingTicks = remainingTicks;
|
||||||
|
this.totalTicks = totalTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRemainingSeconds()
|
||||||
|
{
|
||||||
|
return (int) Math.max(0, Math.round(remainingTicks * 0.6));
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getProgress()
|
||||||
|
{
|
||||||
|
if (totalTicks <= 0)
|
||||||
|
{
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
return Math.max(0.0, Math.min(1.0, (double) remainingTicks / totalTicks));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Client client;
|
||||||
|
private final EssentialsPlusConfig config;
|
||||||
|
|
||||||
|
private final Map<WorldPoint, TrackedTree> trackedTrees = new ConcurrentHashMap<>();
|
||||||
|
private final Map<WorldPoint, RespawnTimer> respawnTimers = new ConcurrentHashMap<>();
|
||||||
|
private final Map<Integer, TreeType> knownTreeIds = new ConcurrentHashMap<>();
|
||||||
|
private final Map<Integer, TreeType> knownStumpIds = new ConcurrentHashMap<>();
|
||||||
|
private final Set<Integer> nonTreeIds = Collections.synchronizedSet(new HashSet<>());
|
||||||
|
|
||||||
|
private TrackedTree activeTree;
|
||||||
|
private int lastLoginTick;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public TreeCooldowns(Client client, EssentialsPlusConfig config)
|
||||||
|
{
|
||||||
|
this.client = client;
|
||||||
|
this.config = config;
|
||||||
|
initKnownIds();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initKnownIds()
|
||||||
|
{
|
||||||
|
// Common stumps
|
||||||
|
registerStump(ObjectID.OAKTREE_STUMP, TreeType.OAK);
|
||||||
|
registerStump(ObjectID.OAK_TREE_STUMP, TreeType.OAK);
|
||||||
|
registerStump(ObjectID.TREE_OAK_STUMP01, TreeType.OAK);
|
||||||
|
|
||||||
|
registerStump(ObjectID.WILLOW_TREE_STUMP, TreeType.WILLOW);
|
||||||
|
registerStump(ObjectID.WILLOW_TREE2OR3OR4_STUMP, TreeType.WILLOW);
|
||||||
|
registerStump(ObjectID.WILLOW_TREE_STUMP_NEW, TreeType.WILLOW);
|
||||||
|
|
||||||
|
registerStump(ObjectID.TEAK_STUMP, TreeType.TEAK);
|
||||||
|
registerStump(ObjectID.TEAK_TREE_STUMP, TreeType.TEAK);
|
||||||
|
registerStump(ObjectID.PRIF_TEAK_STUMP, TreeType.TEAK);
|
||||||
|
registerStump(ObjectID.TEAKTREE_UPDATE_STUMP, TreeType.TEAK);
|
||||||
|
|
||||||
|
registerStump(ObjectID.MAPLE_TREE_STUMP, TreeType.MAPLE);
|
||||||
|
registerStump(ObjectID.MAPLE_TREE_STUMP_NEW, TreeType.MAPLE);
|
||||||
|
registerStump(ObjectID.MAPLETREE_UPDATE_STUMP, TreeType.MAPLE);
|
||||||
|
|
||||||
|
registerStump(ObjectID.HOLLOW_TREE_STUMP, TreeType.HOLLOW);
|
||||||
|
registerStump(ObjectID.HOLLOW_TREE_STUMP_BIG, TreeType.HOLLOW);
|
||||||
|
|
||||||
|
registerStump(ObjectID.MAHOGANY_STUMP, TreeType.MAHOGANY);
|
||||||
|
registerStump(ObjectID.MAHOGANY_TREE_STUMP, TreeType.MAHOGANY);
|
||||||
|
registerStump(ObjectID.PRIF_MAHOGANY_STUMP, TreeType.MAHOGANY);
|
||||||
|
registerStump(ObjectID.MAHOGANYTREE_UPDATE_STUMP, TreeType.MAHOGANY);
|
||||||
|
|
||||||
|
registerStump(ObjectID.ARCTIC_PINE_TREE_STUMP, TreeType.ARCTIC_PINE);
|
||||||
|
|
||||||
|
registerStump(ObjectID.YEWTREE_STUMP, TreeType.YEW);
|
||||||
|
registerStump(ObjectID.YEW_TREE_STUMP, TreeType.YEW);
|
||||||
|
registerStump(ObjectID.YEW_TREE_STUMP_NEW, TreeType.YEW);
|
||||||
|
registerStump(ObjectID.PRIF_YEWTREE_STUMP, TreeType.YEW);
|
||||||
|
registerStump(ObjectID.TREE_YEW_STUMP01, TreeType.YEW);
|
||||||
|
registerStump(ObjectID.YEWTREE_UPDATE_STUMP, TreeType.YEW);
|
||||||
|
registerStump(ObjectID.YEWTREE_UPDATE2_STUMP, TreeType.YEW);
|
||||||
|
|
||||||
|
registerStump(ObjectID.CAMPHOR_TREE_UPDATE_STUMP, TreeType.CAMPHOR);
|
||||||
|
registerStump(ObjectID.CAMPHOR_TREE_STUMP, TreeType.CAMPHOR);
|
||||||
|
|
||||||
|
registerStump(ObjectID.MAGIC_TREE_STUMP, TreeType.MAGIC);
|
||||||
|
registerStump(ObjectID.MAGIC_TREE_STUMP_NEW, TreeType.MAGIC);
|
||||||
|
|
||||||
|
registerStump(ObjectID.IRONWOOD_TREE_UPDATE_STUMP, TreeType.IRONWOOD);
|
||||||
|
registerStump(ObjectID.IRONWOOD_TREE_STUMP, TreeType.IRONWOOD);
|
||||||
|
|
||||||
|
registerStump(ObjectID.REDWOODTREE_L_CUT, TreeType.REDWOOD);
|
||||||
|
registerStump(ObjectID.REDWOODTREE_R_CUT, TreeType.REDWOOD);
|
||||||
|
|
||||||
|
registerStump(ObjectID.ROSEWOOD_TREE_UPDATE_STUMP, TreeType.ROSEWOOD);
|
||||||
|
registerStump(ObjectID.ROSEWOOD_TREE_STUMP, TreeType.ROSEWOOD);
|
||||||
|
|
||||||
|
registerStump(ObjectID.TREESTUMP, TreeType.NORMAL);
|
||||||
|
registerStump(ObjectID.TREESTUMP2, TreeType.NORMAL);
|
||||||
|
registerStump(ObjectID.TREESTUMP2_LIGHT, TreeType.NORMAL);
|
||||||
|
registerStump(ObjectID.TREESTUMP2_GREEN, TreeType.NORMAL);
|
||||||
|
registerStump(ObjectID.TREESTUMP2_SMALL, TreeType.NORMAL);
|
||||||
|
registerStump(ObjectID.TREESTUMP2_BIG, TreeType.NORMAL);
|
||||||
|
registerStump(ObjectID.DEADTREE1_STUMP, TreeType.NORMAL);
|
||||||
|
registerStump(ObjectID.DEADTREE2_STUMP, TreeType.NORMAL);
|
||||||
|
registerStump(ObjectID.DEADTREE3_STUMP, TreeType.NORMAL);
|
||||||
|
registerStump(ObjectID.DEADTREE4_STUMP, TreeType.NORMAL);
|
||||||
|
registerStump(ObjectID.DEADTREE6_STUMP, TreeType.NORMAL);
|
||||||
|
registerStump(ObjectID.EVERGREEN_LARGE_STUMP, TreeType.NORMAL);
|
||||||
|
registerStump(ObjectID.ACHEY_TREE_STUMP, TreeType.NORMAL);
|
||||||
|
registerStump(ObjectID.JUNGLESTUMP_KHARAZI, TreeType.NORMAL);
|
||||||
|
|
||||||
|
// Common trees
|
||||||
|
registerTree(ObjectID.OAKTREE, TreeType.OAK);
|
||||||
|
registerTree(ObjectID.WILLOWTREE, TreeType.WILLOW);
|
||||||
|
registerTree(ObjectID.TEAKTREE, TreeType.TEAK);
|
||||||
|
registerTree(ObjectID.TEAKTREE_UPDATE, TreeType.TEAK);
|
||||||
|
registerTree(ObjectID.MAPLETREE, TreeType.MAPLE);
|
||||||
|
registerTree(ObjectID.MAPLETREE_UPDATE, TreeType.MAPLE);
|
||||||
|
registerTree(ObjectID.HOLLOW_TREE, TreeType.HOLLOW);
|
||||||
|
registerTree(ObjectID.MAHOGANYTREE, TreeType.MAHOGANY);
|
||||||
|
registerTree(ObjectID.MAHOGANYTREE_UPDATE, TreeType.MAHOGANY);
|
||||||
|
registerTree(ObjectID.YEWTREE, TreeType.YEW);
|
||||||
|
registerTree(ObjectID.YEWTREE_UPDATE, TreeType.YEW);
|
||||||
|
registerTree(ObjectID.YEWTREE_UPDATE2, TreeType.YEW);
|
||||||
|
registerTree(ObjectID.CAMPHOR_TREE, TreeType.CAMPHOR);
|
||||||
|
registerTree(ObjectID.MAGICTREE, TreeType.MAGIC);
|
||||||
|
registerTree(ObjectID.IRONWOOD_TREE, TreeType.IRONWOOD);
|
||||||
|
registerTree(ObjectID.REDWOODTREE_L, TreeType.REDWOOD);
|
||||||
|
registerTree(ObjectID.REDWOODTREE_R, TreeType.REDWOOD);
|
||||||
|
registerTree(ObjectID.ROSEWOOD_TREE, TreeType.ROSEWOOD);
|
||||||
|
registerTree(ObjectID.TREE, TreeType.NORMAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerTree(int id, TreeType type)
|
||||||
|
{
|
||||||
|
knownTreeIds.put(id, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerStump(int id, TreeType type)
|
||||||
|
{
|
||||||
|
knownStumpIds.put(id, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset()
|
||||||
|
{
|
||||||
|
trackedTrees.clear();
|
||||||
|
respawnTimers.clear();
|
||||||
|
activeTree = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onGameStateChanged(GameStateChanged event)
|
||||||
|
{
|
||||||
|
if (event.getGameState() != GameState.LOGGED_IN)
|
||||||
|
{
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lastLoginTick = client.getTickCount();
|
||||||
|
scanScene();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onGameTick(GameTick event)
|
||||||
|
{
|
||||||
|
if (!config.treeCooldowns())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Player player = client.getLocalPlayer();
|
||||||
|
if (player == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int currentTick = client.getTickCount();
|
||||||
|
boolean isChopping = WOODCUT_ANIMS.contains(player.getAnimation());
|
||||||
|
|
||||||
|
if (isChopping)
|
||||||
|
{
|
||||||
|
TrackedTree tree = findChoppedTree(player);
|
||||||
|
if (tree != null)
|
||||||
|
{
|
||||||
|
tree.choppedTicks++;
|
||||||
|
tree.lastChoppedTick = currentTick;
|
||||||
|
activeTree = tree;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
activeTree = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Depletion regeneration for idle trees
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
for (TrackedTree tree : trackedTrees.values())
|
||||||
|
{
|
||||||
|
if (tree.choppedTicks > 0 && tree.lastChoppedTick != currentTick)
|
||||||
|
{
|
||||||
|
tree.choppedTicks = Math.max(0, tree.choppedTicks - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prune expired respawn timers
|
||||||
|
respawnTimers.values().removeIf(timer -> now >= timer.deadlineMs + 2000L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onScriptPreFired(ScriptPreFired scriptPreFired)
|
||||||
|
{
|
||||||
|
if (!config.treeCooldowns() || scriptPreFired.getScriptId() != ScriptID.ADD_OVERLAYTIMER_LOC)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object[] args = scriptPreFired.getScriptEvent().getArguments();
|
||||||
|
if (args.length < 6)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int locCoord = (int) args[1];
|
||||||
|
int locId = (int) args[2];
|
||||||
|
int ticks = (int) args[5];
|
||||||
|
|
||||||
|
WorldPoint worldPoint = WorldPoint.fromCoord(locCoord);
|
||||||
|
TreeType treeType = knownStumpIds.get(locId);
|
||||||
|
if (treeType == null)
|
||||||
|
{
|
||||||
|
TrackedTree prev = trackedTrees.get(worldPoint);
|
||||||
|
treeType = (prev != null) ? prev.treeType : TreeType.fromRespawnTicks(ticks);
|
||||||
|
}
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
long durationMs = (long) ticks * 600L;
|
||||||
|
RespawnTimer timer = new RespawnTimer(
|
||||||
|
treeType,
|
||||||
|
worldPoint,
|
||||||
|
now,
|
||||||
|
now + durationMs,
|
||||||
|
ticks,
|
||||||
|
client.getTickCount()
|
||||||
|
);
|
||||||
|
respawnTimers.put(worldPoint, timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onGameObjectSpawned(GameObjectSpawned event)
|
||||||
|
{
|
||||||
|
if (!config.treeCooldowns())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GameObject go = event.getGameObject();
|
||||||
|
if (go == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TreeType treeType = resolveTreeType(go);
|
||||||
|
if (treeType != null)
|
||||||
|
{
|
||||||
|
TrackedTree tree = new TrackedTree(go, treeType);
|
||||||
|
trackedTrees.put(go.getWorldLocation(), tree);
|
||||||
|
// Regrowth: remove any active respawn timer on this tile or within bounds
|
||||||
|
removeRespawnTimersAt(go.getWorldLocation(), go.sizeX(), go.sizeY());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TreeType stumpType = resolveStumpType(go);
|
||||||
|
if (stumpType != null)
|
||||||
|
{
|
||||||
|
WorldPoint pt = go.getWorldLocation();
|
||||||
|
RespawnTimer timer = respawnTimers.get(pt);
|
||||||
|
if (timer != null)
|
||||||
|
{
|
||||||
|
timer.stumpObject = go;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Witnessed felling in player's vicinity
|
||||||
|
Player player = client.getLocalPlayer();
|
||||||
|
int ticksSinceLoad = client.getTickCount() - lastLoginTick;
|
||||||
|
if (player != null && ticksSinceLoad > 2 && player.getWorldLocation().distanceTo(pt) <= 16)
|
||||||
|
{
|
||||||
|
TrackedTree prev = trackedTrees.remove(pt);
|
||||||
|
TreeType actualType = (prev != null) ? prev.treeType : stumpType;
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
long durationMs = (long) (actualType.respawnSec * 1000L);
|
||||||
|
RespawnTimer newTimer = new RespawnTimer(
|
||||||
|
actualType,
|
||||||
|
pt,
|
||||||
|
now,
|
||||||
|
now + durationMs,
|
||||||
|
actualType.respawnTicks,
|
||||||
|
client.getTickCount()
|
||||||
|
);
|
||||||
|
newTimer.stumpObject = go;
|
||||||
|
respawnTimers.put(pt, newTimer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onGameObjectDespawned(GameObjectDespawned event)
|
||||||
|
{
|
||||||
|
if (!config.treeCooldowns())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GameObject go = event.getGameObject();
|
||||||
|
if (go == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WorldPoint pt = go.getWorldLocation();
|
||||||
|
TrackedTree tree = trackedTrees.remove(pt);
|
||||||
|
if (tree != null)
|
||||||
|
{
|
||||||
|
if (!respawnTimers.containsKey(pt))
|
||||||
|
{
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
long durationMs = (long) (tree.treeType.respawnSec * 1000L);
|
||||||
|
RespawnTimer timer = new RespawnTimer(
|
||||||
|
tree.treeType,
|
||||||
|
pt,
|
||||||
|
now,
|
||||||
|
now + durationMs,
|
||||||
|
tree.treeType.respawnTicks,
|
||||||
|
client.getTickCount()
|
||||||
|
);
|
||||||
|
respawnTimers.put(pt, timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If stump despawned, remove respawn timer
|
||||||
|
if (resolveStumpType(go) != null)
|
||||||
|
{
|
||||||
|
respawnTimers.remove(pt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeRespawnTimersAt(WorldPoint origin, int sizeX, int sizeY)
|
||||||
|
{
|
||||||
|
for (int x = 0; x < sizeX; x++)
|
||||||
|
{
|
||||||
|
for (int y = 0; y < sizeY; y++)
|
||||||
|
{
|
||||||
|
respawnTimers.remove(new WorldPoint(origin.getX() + x, origin.getY() + y, origin.getPlane()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TrackedTree findChoppedTree(Player player)
|
||||||
|
{
|
||||||
|
WorldPoint playerLoc = player.getWorldLocation();
|
||||||
|
|
||||||
|
// Prefer continuing with active tree if still adjacent
|
||||||
|
if (activeTree != null && isAdjacent(playerLoc, activeTree))
|
||||||
|
{
|
||||||
|
return activeTree;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find closest adjacent tree aligned with player orientation
|
||||||
|
TrackedTree bestTree = null;
|
||||||
|
int bestScore = Integer.MAX_VALUE;
|
||||||
|
|
||||||
|
int orientation = player.getOrientation(); // 0 = South, 512 = West, 1024 = North, 1536 = East
|
||||||
|
double facingAngle = orientation * (2.0 * Math.PI / 2048.0);
|
||||||
|
double facingDx = -Math.sin(facingAngle);
|
||||||
|
double facingDy = -Math.cos(facingAngle);
|
||||||
|
|
||||||
|
for (TrackedTree tree : trackedTrees.values())
|
||||||
|
{
|
||||||
|
if (tree.worldPoint.getPlane() != playerLoc.getPlane())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAdjacent(playerLoc, tree))
|
||||||
|
{
|
||||||
|
double centerX = tree.worldPoint.getX() + (tree.sizeX - 1) / 2.0;
|
||||||
|
double centerY = tree.worldPoint.getY() + (tree.sizeY - 1) / 2.0;
|
||||||
|
double dx = centerX - playerLoc.getX();
|
||||||
|
double dy = centerY - playerLoc.getY();
|
||||||
|
|
||||||
|
// Dot product with facing direction
|
||||||
|
double dot = dx * facingDx + dy * facingDy;
|
||||||
|
int score = (int) Math.round((1.0 - dot) * 100.0);
|
||||||
|
if (score < bestScore)
|
||||||
|
{
|
||||||
|
bestScore = score;
|
||||||
|
bestTree = tree;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestTree;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isAdjacent(WorldPoint playerLoc, TrackedTree tree)
|
||||||
|
{
|
||||||
|
int minX = tree.worldPoint.getX();
|
||||||
|
int maxX = tree.worldPoint.getX() + tree.sizeX - 1;
|
||||||
|
int minY = tree.worldPoint.getY();
|
||||||
|
int maxY = tree.worldPoint.getY() + tree.sizeY - 1;
|
||||||
|
|
||||||
|
int px = playerLoc.getX();
|
||||||
|
int py = playerLoc.getY();
|
||||||
|
|
||||||
|
int dx = Math.max(0, Math.max(minX - px, px - maxX));
|
||||||
|
int dy = Math.max(0, Math.max(minY - py, py - maxY));
|
||||||
|
|
||||||
|
return dx <= 1 && dy <= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scanScene()
|
||||||
|
{
|
||||||
|
Tile[][][] tiles = client.getScene().getTiles();
|
||||||
|
int plane = client.getPlane();
|
||||||
|
if (tiles == null || plane < 0 || plane >= tiles.length)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Tile[][] planeTiles = tiles[plane];
|
||||||
|
for (Tile[] row : planeTiles)
|
||||||
|
{
|
||||||
|
if (row == null) continue;
|
||||||
|
for (Tile tile : row)
|
||||||
|
{
|
||||||
|
if (tile == null) continue;
|
||||||
|
GameObject[] objects = tile.getGameObjects();
|
||||||
|
if (objects != null)
|
||||||
|
{
|
||||||
|
for (GameObject go : objects)
|
||||||
|
{
|
||||||
|
if (go != null)
|
||||||
|
{
|
||||||
|
TreeType treeType = resolveTreeType(go);
|
||||||
|
if (treeType != null)
|
||||||
|
{
|
||||||
|
trackedTrees.put(go.getWorldLocation(), new TrackedTree(go, treeType));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TreeType resolveTreeType(GameObject obj)
|
||||||
|
{
|
||||||
|
int id = obj.getId();
|
||||||
|
if (nonTreeIds.contains(id))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
TreeType known = knownTreeIds.get(id);
|
||||||
|
if (known != null)
|
||||||
|
{
|
||||||
|
return known;
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectComposition comp = client.getObjectDefinition(id);
|
||||||
|
if (comp == null || comp.getName() == null)
|
||||||
|
{
|
||||||
|
nonTreeIds.add(id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String name = comp.getName().toLowerCase();
|
||||||
|
if (name.contains("stump"))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean hasChop = false;
|
||||||
|
String[] actions = comp.getActions();
|
||||||
|
if (actions != null)
|
||||||
|
{
|
||||||
|
for (String a : actions)
|
||||||
|
{
|
||||||
|
if (a != null && a.equalsIgnoreCase("Chop down"))
|
||||||
|
{
|
||||||
|
hasChop = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasChop && !name.contains("tree") && !name.contains("redwood"))
|
||||||
|
{
|
||||||
|
nonTreeIds.add(id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
TreeType matched = matchTreeName(name);
|
||||||
|
if (matched != null)
|
||||||
|
{
|
||||||
|
knownTreeIds.put(id, matched);
|
||||||
|
return matched;
|
||||||
|
}
|
||||||
|
|
||||||
|
nonTreeIds.add(id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TreeType resolveStumpType(GameObject obj)
|
||||||
|
{
|
||||||
|
int id = obj.getId();
|
||||||
|
TreeType known = knownStumpIds.get(id);
|
||||||
|
if (known != null)
|
||||||
|
{
|
||||||
|
return known;
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectComposition comp = client.getObjectDefinition(id);
|
||||||
|
if (comp == null || comp.getName() == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String name = comp.getName().toLowerCase();
|
||||||
|
if (name.contains("stump"))
|
||||||
|
{
|
||||||
|
TreeType matched = matchTreeName(name);
|
||||||
|
TreeType result = (matched != null) ? matched : TreeType.NORMAL;
|
||||||
|
knownStumpIds.put(id, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TreeType matchTreeName(String name)
|
||||||
|
{
|
||||||
|
if (name.contains("redwood")) return TreeType.REDWOOD;
|
||||||
|
if (name.contains("magic")) return TreeType.MAGIC;
|
||||||
|
if (name.contains("yew")) return TreeType.YEW;
|
||||||
|
if (name.contains("camphor")) return TreeType.CAMPHOR;
|
||||||
|
if (name.contains("ironwood")) return TreeType.IRONWOOD;
|
||||||
|
if (name.contains("rosewood")) return TreeType.ROSEWOOD;
|
||||||
|
if (name.contains("arctic pine")) return TreeType.ARCTIC_PINE;
|
||||||
|
if (name.contains("mahogany")) return TreeType.MAHOGANY;
|
||||||
|
if (name.contains("maple")) return TreeType.MAPLE;
|
||||||
|
if (name.contains("hollow")) return TreeType.HOLLOW;
|
||||||
|
if (name.contains("teak")) return TreeType.TEAK;
|
||||||
|
if (name.contains("willow")) return TreeType.WILLOW;
|
||||||
|
if (name.contains("oak")) return TreeType.OAK;
|
||||||
|
if (name.contains("tree") || name.contains("evergreen")) return TreeType.NORMAL;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<RespawnTimer> getActiveRespawnTimers()
|
||||||
|
{
|
||||||
|
return respawnTimers.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<ChopTimer> getActiveChopTimers()
|
||||||
|
{
|
||||||
|
List<ChopTimer> result = new ArrayList<>();
|
||||||
|
for (TrackedTree tree : trackedTrees.values())
|
||||||
|
{
|
||||||
|
if (tree.treeType.hasChopTimer() && tree.choppedTicks > 0)
|
||||||
|
{
|
||||||
|
int totalTicks = tree.treeType.getDespawnTicks();
|
||||||
|
int remaining = totalTicks - tree.choppedTicks;
|
||||||
|
if (remaining > 0)
|
||||||
|
{
|
||||||
|
result.add(new ChopTimer(tree.treeType, tree.worldPoint, tree.gameObject, remaining, totalTicks));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,445 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import java.security.InvalidParameterException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.Getter;
|
||||||
|
import net.runelite.api.gameval.AnimationID;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
public enum AnimationData
|
||||||
|
{
|
||||||
|
// MELEE
|
||||||
|
MELEE_VIGGORAS_CHAINMACE(AnimationID.WILD_CAVE_CHAINMACE_CRUSH, AttackStyle.MELEE),
|
||||||
|
MELEE_DAGGER_SLASH(AnimationID.HUMAN_DDAGGER_LUNGE, AttackStyle.MELEE),
|
||||||
|
MELEE_SPEAR_STAB(AnimationID.HUMAN_DSPEAR_STAB, AttackStyle.MELEE),
|
||||||
|
MELEE_SWORD_STAB(AnimationID.HUMAN_SWORD_STAB, AttackStyle.MELEE),
|
||||||
|
MELEE_SCIM_SLASH(AnimationID.HUMAN_SWORD_SLASH, AttackStyle.MELEE),
|
||||||
|
|
||||||
|
MELEE_LANCE_STAB(AnimationID.HUMAN_DHUNTER_LANCE_ATTACK, AttackStyle.MELEE),
|
||||||
|
MELEE_LANCE_CRUSH(AnimationID.HUMAN_DHUNTER_LANCE_CRUSH, AttackStyle.MELEE),
|
||||||
|
MELEE_LANCE_SLASH(AnimationID.HUMAN_DHUNTER_LANCE_SLASH, AttackStyle.MELEE),
|
||||||
|
|
||||||
|
MELEE_FANG_STAB(AnimationID.HUMAN_OSMUMTENS_FANG, AttackStyle.MELEE),
|
||||||
|
MELEE_FANG_SPEC(AnimationID.OLAF2_BRINE_SABRE_SPECIAL, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
|
||||||
|
MELEE_GENERIC_SLASH(AnimationID.HUMAN_AXE_CHOP, AttackStyle.MELEE),
|
||||||
|
|
||||||
|
MELEE_BATTLEAXE_SLASH(AnimationID.HUMAN_AXE_HACK, AttackStyle.MELEE),
|
||||||
|
MELEE_MACE_STAB(AnimationID.HUMAN_BLUNT_SPIKE, AttackStyle.MELEE),
|
||||||
|
MELEE_BATTLEAXE_CRUSH(AnimationID.HUMAN_BLUNT_POUND, AttackStyle.MELEE),
|
||||||
|
MELEE_2H_CRUSH(AnimationID.HUMAN_DHSWORD_CHOP, AttackStyle.MELEE),
|
||||||
|
MELEE_2H_SLASH(AnimationID.HUMAN_DHSWORD_SLASH, AttackStyle.MELEE),
|
||||||
|
MELEE_STAFF_CRUSH_2(AnimationID.HUMAN_STAFF_PUMMEL, AttackStyle.MELEE),
|
||||||
|
MELEE_STAFF_CRUSH_3(AnimationID.HUMAN_STAFFORB_PUMMEL, AttackStyle.MELEE),
|
||||||
|
MELEE_PUNCH(AnimationID.HUMAN_UNARMEDPUNCH, AttackStyle.MELEE),
|
||||||
|
MELEE_KICK(AnimationID.HUMAN_UNARMEDKICK, AttackStyle.MELEE),
|
||||||
|
MELEE_STAFF_STAB(AnimationID.HUMAN_SPEAR_SPIKE, AttackStyle.MELEE),
|
||||||
|
MELEE_SPEAR_CRUSH(AnimationID.HUMAN_SPEAR_LUNGE, AttackStyle.MELEE),
|
||||||
|
MELEE_STAFF_SLASH(AnimationID.HUMAN_SCYTHE_SWEEP, AttackStyle.MELEE),
|
||||||
|
MELEE_DLONG_SPEC(AnimationID.CLEAVE, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_DRAGON_MACE_SPEC(AnimationID.SHATTER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_DRAGON_DAGGER_SPEC(AnimationID.PUNCTURE, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_DRAGON_WARHAMMER_SPEC(AnimationID.DRAGON_WARHAMMER_SA_PLAYER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_ABYSSAL_WHIP(AnimationID.SLAYER_ABYSSAL_WHIP_ATTACK, AttackStyle.MELEE),
|
||||||
|
MELEE_GRANITE_MAUL(AnimationID.SLAYER_GRANITE_MAUL_ATTACK, AttackStyle.MELEE),
|
||||||
|
MELEE_GRANITE_MAUL_SPEC(AnimationID.SLAYER_GRANITE_MAUL_SPECIAL_ATTACK, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_DHAROKS_GREATAXE_CRUSH(AnimationID.BARROW_DHAROK_SLASH, AttackStyle.MELEE),
|
||||||
|
MELEE_DHAROKS_GREATAXE_SLASH(AnimationID.BARROW_DHAROK_CRUSH, AttackStyle.MELEE),
|
||||||
|
MELEE_AHRIMS_STAFF_CRUSH(AnimationID.BARROWS_QUARTERSTAFF_ATTACK, AttackStyle.MELEE),
|
||||||
|
MELEE_OBBY_MAUL_CRUSH(AnimationID.DRAGON_PICKAXE_ANIM, AttackStyle.MELEE),
|
||||||
|
MELEE_ABYSSAL_DAGGER_STAB(AnimationID.ABYSSAL_DAGGER_LUNGE, AttackStyle.MELEE),
|
||||||
|
MELEE_ABYSSAL_BLUDGEON_CRUSH(AnimationID.ABYSSAL_BLUDGEON_CRUSH, AttackStyle.MELEE),
|
||||||
|
MELEE_ABYSSAL_BLUDGEON_SPEC(AnimationID.ABYSSAL_BLUDGEON_SPECIAL_ATTACK, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_LEAF_BLADED_BATTLEAXE_CRUSH(AnimationID.BATTLEAXE_CRUSH, AttackStyle.MELEE),
|
||||||
|
MELEE_INQUISITORS_MACE(AnimationID.HUMAN_INQUISITORS_MACE_CRUSH, AttackStyle.MELEE),
|
||||||
|
MELEE_BARRELCHEST_ANCHOR_CRUSH(AnimationID.BRAIN_PLAYER_ANCHOR_ATTACK, AttackStyle.MELEE),
|
||||||
|
MELEE_BARRELCHEST_ANCHOR_CRUSH_SPEC(AnimationID.BRAIN_PLAYER_ANCHOR_SPECIAL_ATTACK, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_LEAF_BLADED_BATTLEAXE_SLASH(AnimationID.GODWARS_GODSWORD_ZAMORAK_PLAYER, AttackStyle.MELEE),
|
||||||
|
MELEE_GODSWORD_SLASH(AnimationID.DH_SWORD_UPDATE_SLASH, AttackStyle.MELEE),
|
||||||
|
MELEE_GODSWORD_CRUSH(AnimationID.DH_SWORD_UPDATE_SMASH, AttackStyle.MELEE),
|
||||||
|
MELEE_GODSWORD_DEFENSIVE(AnimationID.DH_SWORD_UPDATE_BLOCK, AttackStyle.MELEE),
|
||||||
|
MELEE_RUNE_CLAWS_SPEC(AnimationID.IMPALE, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_DRAGON_CLAWS_SPEC(AnimationID.HUMAN_DRAGON_CLAWS_SPEC, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_VLS_SPEC(AnimationID.HUMAN_DRAGON_SWORD_SPEC, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_ELDER_MAUL(AnimationID.HUMAN_ELDER_MAUL_ATTACK, AttackStyle.MELEE),
|
||||||
|
MELEE_ZAMORAK_GODSWORD_SPEC(AnimationID.ZGS_SPECIAL_PLAYER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_ELDER_MAUL_SPEC(AnimationID.HUMAN_ELDER_MAUL_SPEC, AttackStyle.MELEE),
|
||||||
|
MELEE_ZAMORAK_GODSWORD_OR_SPEC(AnimationID.ZGS_SPECIAL_ORNATE_PLAYER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_SARADOMIN_GODSWORD_SPEC(AnimationID.SGS_SPECIAL_PLAYER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_SARADOMIN_GODSWORD_OR_SPEC(AnimationID.SGS_SPECIAL_ORNATE_PLAYER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_BANDOS_GODSWORD_SPEC(AnimationID.BGS_SPECIAL_PLAYER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_BANDOS_GODSWORD_OR_SPEC(AnimationID.BGS_SPECIAL_ORNATE_PLAYER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_ARMADYL_GODSWORD_SPEC(AnimationID.AGS_SPECIAL_PLAYER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_ARMADYL_GODSWORD_OR_SPEC(AnimationID.AGS_SPECIAL_ORNATE_PLAYER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_SCYTHE(AnimationID.SCYTHE_OF_VITUR_ATTACK, AttackStyle.MELEE),
|
||||||
|
MELEE_GHAZI_RAPIER_STAB(AnimationID.GHRAZI_RAPIER_ATTACK, AttackStyle.MELEE),
|
||||||
|
MELEE_ANCIENT_GODSWORD_SPEC(AnimationID.NGS_SPECIAL_PLAYER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_CRYSTAL_HALBERD_SPEC(AnimationID.DRAGON_HALBERD_SPECIAL_ATTACK, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_SOULREAPER_AXE(AnimationID.ANCIENT_AXE_CRUSH, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_SOULREAPER_AXE_SPEC(AnimationID.ANCIENT_AXE_SPECIAL, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_GUTHANS_LUNGE(AnimationID.BARROWS_WAR_SPEAR_STAB, AttackStyle.MELEE),
|
||||||
|
MELEE_GUTHANS_SWIPE(AnimationID.BARROWS_WAR_SPEAR_SLASH, AttackStyle.MELEE),
|
||||||
|
MELEE_GUTHANS_POUNDMA(AnimationID.BARROWS_WAR_SPEAR_CRUSH, AttackStyle.MELEE),
|
||||||
|
MELEE_TORAG_HAMMERS(AnimationID.BARROW_TORAG_CRUSH, AttackStyle.MELEE),
|
||||||
|
MELEE_VERACS_FLAIL(AnimationID.BARROW_GUTHAN_CRUSH, AttackStyle.MELEE),
|
||||||
|
MELEE_BLISTERWOOD_FLAIL_CRUSH(AnimationID.IVANDIS_FLAIL_ATTACK, AttackStyle.MELEE),
|
||||||
|
MELEE_BONE_DAGGER_SPEC(AnimationID.DTTD_PLAYER_STAB_BONE_DAGGER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_DUAL_MACUAHUITL(AnimationID.PMOON_MACUAHUITL_CRUSH, AttackStyle.MELEE),
|
||||||
|
MELEE_BLUE_MOON_SPEAR_SPEC(AnimationID.HUMAN_ZAMORAKSPEAR_LUNGE, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_BLUE_MOON_SPEAR(AnimationID.HUMAN_ZAMORAKSPEAR_STAB, AttackStyle.MELEE),
|
||||||
|
MELEE_DHINS(AnimationID.HUMAN_DINHS_BULWARK_BASH, AttackStyle.MELEE),
|
||||||
|
MELEE_URSINE_CHAINMACE_SPEC(AnimationID.HUMAN_SPECIAL02_URSINE, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_ANCIENT_MACE_SPEC(AnimationID.SLICE_PLAYER_MACE_SPECIAL_ATTACK, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_DSCIM_SPEC(AnimationID.SP_ATTACK_DRAGON_SCIMITAR, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_D2H_SPEC(AnimationID.DRAGON_TWO_HANDED_SWORD, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_ARCLIGHT_SPEC(AnimationID.DARK_SPEC_PLAYER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_SARA_SWORD_SPEC(AnimationID.SARADOMIN_SWORD_SPECIAL_PLAYER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_RED_KERIS_SPEC(AnimationID.TOA_KERIS_PARTISAN_SPECIAL01, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MELEE_SALAMANDER(AnimationID.HUMAN_ATTACK_SALAMANDER, AttackStyle.MELEE),
|
||||||
|
MELEE_INFERNAL_TECPATL(AnimationID.TECPATL_STAB, AttackStyle.MELEE),
|
||||||
|
MELEE_HALLOWED_FLAIL(AnimationID.HUMAN_WEAPONS_HALLOWED_FLAIL01_ATTACK01, AttackStyle.MELEE),
|
||||||
|
MELEE_FELLING_AXE(AnimationID.FORESTRY_2H_AXE_ATTACK, AttackStyle.MELEE),
|
||||||
|
MELEE_HALLOWFELL(AnimationID.HUMAN_HALLOWFELL_SLASH, AttackStyle.MELEE),
|
||||||
|
|
||||||
|
// RANGED
|
||||||
|
RANGED_CHINCHOMPA(AnimationID.HUMAN_CHINCHOMPA_ATTACK_PVN, AttackStyle.RANGED),
|
||||||
|
RANGED_SHORTBOW(AnimationID.HUMAN_BOW, AttackStyle.RANGED, MetaData.STANDARD_BOW_ATTACK.d),
|
||||||
|
RANGED_RUNE_KNIFE_PVP(AnimationID.HUMAN_STAKE2, AttackStyle.RANGED),
|
||||||
|
RANGED_MAGIC_SHORTBOW_SPEC(AnimationID.SNAPSHOT, AttackStyle.RANGED, MetaData.STANDARD_BOW_ATTACK.d | MetaData.SPECIAL_ATTACK.d),
|
||||||
|
RANGED_CROSSBOW_PVP(AnimationID.XBOWS_HUMAN_FIRE_AND_RELOAD, AttackStyle.RANGED),
|
||||||
|
RANGED_BLOWPIPE(AnimationID.SNAKEBOSS_BLOWPIPE_ATTACK, AttackStyle.RANGED),
|
||||||
|
RANGED_DARTS(AnimationID.II_HUMAN_DART_THROW_PVN, AttackStyle.RANGED),
|
||||||
|
RANGED_BALLISTA(AnimationID.BALLISTA_ATTACK, AttackStyle.RANGED),
|
||||||
|
RANGED_BALLISTA_SPEC(AnimationID.BALLISTA_SPECIAL_ATTACK_PVN, AttackStyle.RANGED, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
RANGED_RUNE_THROWNAXE_SPEC(AnimationID.CHAINHIT, AttackStyle.RANGED, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
RANGED_DRAGON_THROWNAXE_SPEC(AnimationID.HUMAN_DRAGON_TAXE_SPEC, AttackStyle.RANGED, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
RANGED_RUNE_CROSSBOW(AnimationID.XBOWS_HUMAN_FIRE_AND_RELOAD_PVN, AttackStyle.RANGED),
|
||||||
|
RANGED_RUNE_CROSSBOW_OR(AnimationID.HUMAN_XBOWS_LEAGUE03_ATTACK_PVN, AttackStyle.RANGED),
|
||||||
|
RANGED_BALLISTA_2(AnimationID.BALLISTA_ATTACK_PVN, AttackStyle.RANGED),
|
||||||
|
RANGED_RUNE_KNIFE(AnimationID.HUMAN_STAKE2_PVN, AttackStyle.RANGED),
|
||||||
|
RANGED_DRAGON_KNIFE(AnimationID.HUMAN_DRAGON_KNIFE, AttackStyle.RANGED),
|
||||||
|
RANGED_DRAGON_KNIFE_SPEC(AnimationID.HUMAN_DRAGON_TKNIVES_SPEC, AttackStyle.RANGED, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
RANGED_DRAGON_KNIFE_POISONED(AnimationID.HUMAN_DRAGON_KNIFE_P, AttackStyle.RANGED),
|
||||||
|
RANGED_DRAGON_KNIFE_POISONED_SPEC(AnimationID.HUMAN_DRAGON_TKNIVES_SPEC_POISON, AttackStyle.RANGED, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
RANGED_ZARYTE_CROSSBOW(AnimationID.ZCB_ATTACK_PVN, AttackStyle.RANGED),
|
||||||
|
RANGED_ZARYTE_CROSSBOW_PVP(AnimationID.ZCB_ATTACK, AttackStyle.RANGED),
|
||||||
|
RANGED_BLAZING_BLOWPIPE(AnimationID.SNAKEBOSS_BLOWPIPE_ATTACK_ORNAMENT, AttackStyle.RANGED),
|
||||||
|
RANGED_VENATOR_BOW(AnimationID.HUMAN_WEAPON_BOW_VENATOR01_SHOOT, AttackStyle.RANGED, MetaData.STANDARD_BOW_ATTACK.d),
|
||||||
|
RANGED_KARIL_CROSSBOW(AnimationID.BARROWS_REPEATING_CROSSBOW_FIRE, AttackStyle.RANGED),
|
||||||
|
RANGED_ATLATL(AnimationID.HUMAN_ATLATL_ATTACK_RANGED_01, AttackStyle.RANGED, MetaData.STANDARD_BOW_ATTACK.d),
|
||||||
|
RANGED_ATLATL_SPEC(AnimationID.HUMAN_SPECIAL_ATLATL_01, AttackStyle.RANGED, MetaData.STANDARD_BOW_ATTACK.d | MetaData.SPECIAL_ATTACK.d),
|
||||||
|
RANGED_TONALZTICS(AnimationID.HUMAN_GLAIVE_RALOS01_CHARGED_THROW, AttackStyle.RANGED),
|
||||||
|
RANGED_TONALZTICS_SPEC(AnimationID.HUMAN_GLAIVE_RALOS01_CHARGED_SPECIAL, AttackStyle.RANGED, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
RANGED_WEBWEAVER_SPEC(AnimationID.HUMAN_SPECIAL01_WEBWEAVER, AttackStyle.RANGED, MetaData.STANDARD_BOW_ATTACK.d | MetaData.SPECIAL_ATTACK.d),
|
||||||
|
RANGED_BONE_CROSSBOW_SPEC(AnimationID.DTTD_PLAYER_FIRE_BONE_CROSSBOW_PVN, AttackStyle.RANGED, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
RANGED_SCORCHING_BOW_SPEC(AnimationID.HUMAN_WEAPON_BOW_SCORCHED_01_SPEC, AttackStyle.RANGED, MetaData.STANDARD_BOW_ATTACK.d | MetaData.SPECIAL_ATTACK.d),
|
||||||
|
|
||||||
|
// MAGIC
|
||||||
|
MAGIC_GOD_SPELL(AnimationID.HUMAN_CASTING, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_IBAN_BLAST(AnimationID.HUMAN_CASTIBANBLAST, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_SLAYER_DART(AnimationID.SLAYER_MAGICDART_CAST, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_BIND(AnimationID.HUMAN_CASTENTANGLE, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_BIND_STAFF(AnimationID.HUMAN_CASTENTANGLE_STAFF, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_CONFUSE(AnimationID.HUMAN_CASTCONFUSE, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_CONFUSE_STAFF(AnimationID.HUMAN_CASTCONFUSE_STAFF, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_CONFUSE_STAFF_WALK(AnimationID.HUMAN_CASTCONFUSE_STAFF_WALKMERGE, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_CONFUSE_WALK(AnimationID.HUMAN_CASTCONFUSE_WALKMERGE, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_CRUMBLE_UNDEAD(AnimationID.HUMAN_CASTCRUMBLEUNDEAD, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_CRUMBLE_UNDEAD_HOLDING_STAFF(AnimationID.HUMAN_CASTCRUMBLEUNDEAD_STAFF, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_ENFEEBLE(AnimationID.HUMAN_CASTENFEEBLE_STAFF, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_STRIKE_BOLT_BLAST(AnimationID.HUMAN_CASTSTRIKE, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_STRIKE_BOLT_BLAST_STAFF(AnimationID.HUMAN_CASTSTRIKE_STAFF, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_STRIKE_BOLT_BLAST_STAFF_WALK(AnimationID.HUMAN_CASTSTRIKE_STAFF_WALKMERGE, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_STRIKE_BOLT_BLAST_WALK(AnimationID.HUMAN_CASTSTRIKE_WALKMERGE, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_STUN(AnimationID.HUMAN_CASTSTUN_STAFF, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_SURGE(AnimationID.HUMAN_CAST_SURGE, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_SURGE_STAFF(AnimationID.HUMAN_CAST_SURGE_FAST, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_SURGE_STAFF_WALK(AnimationID.HUMAN_CAST_SURGE_WALKMERGE, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_VULNERABILITY_CURSE(AnimationID.HUMAN_CASTCURSE, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_VULNERABILITY_CURSE_STAFF(AnimationID.HUMAN_CASTCURSE_STAFF, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_VULNERABILITY_CURSE_STAFF_WALK(AnimationID.HUMAN_CASTCURSE_STAFF_WALKMERGE, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_VULNERABILITY_CURSE_WALK(AnimationID.HUMAN_CASTCURSE_WALKMERGE, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_WAVE(AnimationID.HUMAN_CASTWAVE, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_WAVE_STAFF(AnimationID.HUMAN_CASTWAVE_STAFF, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_WAVE_STAFF_WALK(AnimationID.HUMAN_CASTWAVE_STAFF_WALKMERGE, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_WAVE_WALK(AnimationID.HUMAN_CASTWAVE_WALKMERGE, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_WEAKEN(AnimationID.HUMAN_CASTWEAKEN, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_WEAKEN_STAFF(AnimationID.HUMAN_CASTWEAKEN_STAFF, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_WEAKEN_STAFF_WALK(AnimationID.HUMAN_CASTWEAKEN_STAFF_WALKMERGE, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_STANDARD_WEAKEN_WALK(AnimationID.HUMAN_CASTWEAKEN_WALKMERGE, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d),
|
||||||
|
|
||||||
|
MAGIC_ANCIENT_MULTI_TARGET(AnimationID.ZAROS_VERTICAL_CASTING_WALKMERGE, AttackStyle.MAGIC, Spellbook.ANCIENT, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_ANCIENT_MULTI_TARGET_PVP(AnimationID.ZAROS_VERTICAL_CASTING, AttackStyle.MAGIC, Spellbook.ANCIENT, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_ANCIENT_SINGLE_TARGET(AnimationID.ZAROS_CASTING_WALKMERGE, AttackStyle.MAGIC, Spellbook.ANCIENT, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_ANCIENT_SINGLE_TARGET_PVP(AnimationID.ZAROS_CASTING, AttackStyle.MAGIC, Spellbook.ANCIENT, MetaData.NO_DATA.d),
|
||||||
|
|
||||||
|
MAGIC_ARCEUUS_DEMONBANE(AnimationID.HUMAN_SPELLCAST_DEMONBANE, AttackStyle.MAGIC, Spellbook.ARCEUUS, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_ARCEUUS_GRASP(AnimationID.HUMAN_SPELLCAST_GRASP, AttackStyle.MAGIC, Spellbook.ARCEUUS, MetaData.NO_DATA.d),
|
||||||
|
|
||||||
|
MAGIC_ACCURSED_SCEPTRE_SPEC(AnimationID.HUMAN_SPECIAL_ACCURSED, AttackStyle.MAGIC, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MAGIC_EYE_OF_AYAK(AnimationID.HUMAN_EYE_OF_AYAK_NORMAL, AttackStyle.MAGIC, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_EYE_OF_AYAK_SPEC(AnimationID.HUMAN_EYE_OF_AYAK_SPECIAL, AttackStyle.MAGIC, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MAGIC_TUMEKENS_SHADOW(AnimationID.TOA_SOT_CAST_B, AttackStyle.MAGIC, MetaData.NO_DATA.d),
|
||||||
|
MAGIC_VOLATILE_NIGHTMARE_STAFF_SPEC(AnimationID.NIGHTMARE_STAFF_SPECIAL, AttackStyle.MAGIC, MetaData.SPECIAL_ATTACK.d),
|
||||||
|
MAGIC_WARPED_SCEPTRE(AnimationID.POG_WARPED_SCEPTRE_ATTACK, AttackStyle.MAGIC, MetaData.NO_DATA.d),
|
||||||
|
|
||||||
|
// Misc Non-attacks
|
||||||
|
MAGIC_IMBUE(AnimationID.HUMAN_CASTBONESTOBANANAS, AttackStyle.NON_ATTACK),
|
||||||
|
SPELLBOOK_SWAP(AnimationID.DREAM_PLAYER_SPELLBOOK_SWAP, AttackStyle.NON_ATTACK),
|
||||||
|
LUNAR_GROUP(AnimationID.QUEST_LUNAR_SPELL_CAST_SPELL_ON_GROUP, AttackStyle.NON_ATTACK),
|
||||||
|
LUNAR_OTHER(AnimationID.QUEST_LUNAR_PUSHING_MAGIC_ANIMATION, AttackStyle.NON_ATTACK),
|
||||||
|
NPC_CONTACT(AnimationID.LUNAR_HUMAN_MAGIC_SUMMON2, AttackStyle.NON_ATTACK),
|
||||||
|
VENGEANCE_1(AnimationID.VENGEANCE_SPELL_ANIM_NOSTALLING, AttackStyle.NON_ATTACK),
|
||||||
|
VENGEANCE_2(AnimationID.VENGEANCE_SPELL_ANIM_STALLING, AttackStyle.NON_ATTACK),
|
||||||
|
REANIMATION(AnimationID.ARCEUUS_NECROMANCY_PLAYERANIM, AttackStyle.NON_ATTACK),
|
||||||
|
DEMONIC_OFFERING(AnimationID.HUMAN_CAST_OFFERING, AttackStyle.NON_ATTACK),
|
||||||
|
SHADOW_VEIL(AnimationID.HUMAN_SPELLCAST_SHADOWVEIL, AttackStyle.NON_ATTACK),
|
||||||
|
MARK_OF_DARKNESS(AnimationID.HUMAN_CAST_SELFIMBUE, AttackStyle.NON_ATTACK),
|
||||||
|
PICK_POCKETING(AnimationID.HUMAN_PICKPOCKET, AttackStyle.NON_ATTACK),
|
||||||
|
SUMMON_THRALL(AnimationID.HUMAN_SPELLCAST_RESURRECT, AttackStyle.NON_ATTACK),
|
||||||
|
LUNAR_TELEPORT(AnimationID.HUMAN_TELEPORT_OTHER_IMPACT, AttackStyle.NON_ATTACK),
|
||||||
|
MONSTER_EXAMINE(AnimationID.DREAM_PLAYER_MONSTEREXAM_STATSPY, AttackStyle.NON_ATTACK),
|
||||||
|
HUMIDIFY(AnimationID.DREAM_PLAYER_HUMIDIFY_SPELL, AttackStyle.NON_ATTACK),
|
||||||
|
GEOMANCY(AnimationID.LUNAR_HUMAN_MAGIC_GEOMANCY, AttackStyle.NON_ATTACK),
|
||||||
|
DREAM(AnimationID.FOSSIL_LOC_CLAM_IDLE_SHUT, AttackStyle.NON_ATTACK),
|
||||||
|
ROCKSLUG_BAG_OF_SALT(AnimationID.SLAYER_SALT_SPRINKLE, AttackStyle.NON_ATTACK),
|
||||||
|
DESSET_LIZARD_ICE_COOLER(AnimationID.HUMAN_CHINCHOMPA_ATTACK, AttackStyle.NON_ATTACK),
|
||||||
|
FLECTHING_KNIFE(AnimationID.HUMAN_FLETCHING, AttackStyle.NON_ATTACK),
|
||||||
|
FLECTHING_KEBBIT(AnimationID.HUMAN_CRAFTING_SPIKEDVAMBRACES, AttackStyle.NON_ATTACK),
|
||||||
|
FLECTHING_CHISEL(AnimationID.HUMAN_FLETCHING_HUNTINGBOLTS, AttackStyle.NON_ATTACK),
|
||||||
|
FLECTHING_DART_TIP_1(AnimationID.HUMAN_FLETCHING_ADD_DART_FEATHERS_BRONZE, AttackStyle.NON_ATTACK),
|
||||||
|
FLECTHING_DART_TIP_2(AnimationID.HUMAN_FLETCHING_ADD_DART_FEATHERS_IRON, AttackStyle.NON_ATTACK),
|
||||||
|
FLECTHING_DART_TIP_3(AnimationID.HUMAN_FLETCHING_ADD_DART_FEATHERS_STEEL, AttackStyle.NON_ATTACK),
|
||||||
|
FLECTHING_DART_TIP_4(AnimationID.HUMAN_FLETCHING_ADD_DART_FEATHERS_MITHRIL, AttackStyle.NON_ATTACK),
|
||||||
|
FLECTHING_DART_TIP_5(AnimationID.HUMAN_FLETCHING_ADD_DART_FEATHERS_ADAMANT, AttackStyle.NON_ATTACK),
|
||||||
|
FLECTHING_DART_TIP_6(AnimationID.HUMAN_FLETCHING_ADD_DART_FEATHERS_RUNE, AttackStyle.NON_ATTACK),
|
||||||
|
FLECTHING_DART_TIP_7(AnimationID.HUMAN_FLETCHING_ADD_DART_FEATHERS_DRAGON, AttackStyle.NON_ATTACK),
|
||||||
|
FLECTHING_DART_TIP_8(AnimationID.HUMAN_FLETCHING_ADD_DART_FEATHERS_AMETHYST, AttackStyle.NON_ATTACK),
|
||||||
|
HERB_TAR(AnimationID.HUMAN_SALAMANDER_TAR_GRIND, AttackStyle.NON_ATTACK),
|
||||||
|
SETUP_HUNTER_TRAP(AnimationID.HUMAN_LAYTRAP, AttackStyle.NON_ATTACK),
|
||||||
|
RESET_SNARE_TRAP(AnimationID.HUMAN_HUNTING_DISMANTLE_NET, AttackStyle.NON_ATTACK),
|
||||||
|
RESET_BOX_TRAP(AnimationID.HUNTING_SETTING_TRAP_SMALL, AttackStyle.NON_ATTACK),
|
||||||
|
DESERT_AMMY(AnimationID.TELEPORT_NARDAH_HUMAN, AttackStyle.NON_ATTACK),
|
||||||
|
EAT_FOOD_OR_POTION(AnimationID.HUMAN_EAT, AttackStyle.NON_ATTACK),
|
||||||
|
OVERLOAD_HIT(AnimationID.HUMAN_KILLERWATT_ELECTRICSHOCK, AttackStyle.NON_ATTACK),
|
||||||
|
TAKING_HIT_1HANDED_UNARMED(AnimationID.HUMAN_AXE_BLOCK, AttackStyle.NON_ATTACK),
|
||||||
|
TAKING_HIT_2H_SWORD(AnimationID.HUMAN_DHSWORD_BLOCK, AttackStyle.NON_ATTACK),
|
||||||
|
TAKING_HIT_ANCHOR(AnimationID.BRAIN_PLAYER_ANCHOR_DEFEND, AttackStyle.NON_ATTACK),
|
||||||
|
TAKING_HIT_BLISTERWOOD_FLAIL(AnimationID.IVANDIS_FLAIL_DEFEND, AttackStyle.NON_ATTACK),
|
||||||
|
TAKING_HIT_BLOWPIPE(AnimationID.HUMAN_SPEAR_BLOCK, AttackStyle.NON_ATTACK),
|
||||||
|
TAKING_HIT_BULWARK(AnimationID.HUMAN_DINHS_BULWARK_BLOCK, AttackStyle.NON_ATTACK),
|
||||||
|
TAKING_HIT_CHAINMACE(AnimationID.WILD_CAVE_CHAINMACE_DEFEND, AttackStyle.NON_ATTACK),
|
||||||
|
TAKING_HIT_CHIN_CHOMPA(AnimationID.HUMAN_CHINCHOMPA_DEFEND, AttackStyle.NON_ATTACK),
|
||||||
|
TAKING_HIT_DAGGER(AnimationID.HUMAN_DDAGGER_BLOCK, AttackStyle.NON_ATTACK),
|
||||||
|
TAKING_HIT_DEFENDER(AnimationID.WARGUILD_PARRY_DEFEND, AttackStyle.NON_ATTACK),
|
||||||
|
TAKING_HIT_FANG(AnimationID.HUMAN_SWORD_DEF, AttackStyle.NON_ATTACK),
|
||||||
|
TAKING_HIT_GODSWORD(AnimationID.DH_SWORD_UPDATE_DEFEND, AttackStyle.NON_ATTACK),
|
||||||
|
TAKING_HIT_KERIS(AnimationID.HUMAN_DSPEAR_BLOCK, AttackStyle.NON_ATTACK),
|
||||||
|
TAKING_HIT_LARGE_STAFF(AnimationID.HUMAN_STAFFORB_BLOCK, AttackStyle.NON_ATTACK),
|
||||||
|
TAKING_HIT_MACE(AnimationID.HUMAN_BLUNT_BLOCK, AttackStyle.NON_ATTACK),
|
||||||
|
TAKING_HIT_OBBY_MAUL(AnimationID.SLAYER_GRANITE_MAUL_DEFEND, AttackStyle.NON_ATTACK),
|
||||||
|
TAKING_HIT_SCYTHE(AnimationID.HUMAN_SCYTHE_BLOCK, AttackStyle.NON_ATTACK),
|
||||||
|
TAKING_HIT_SHIELD(AnimationID.HUMAN_SHIELD_DEFENCE, AttackStyle.NON_ATTACK),
|
||||||
|
TAKING_HIT_SPEAR(AnimationID.HUMAN_ZAMORAKSPEAR_BLOCK, AttackStyle.NON_ATTACK),
|
||||||
|
TAKING_HIT_STAFF(AnimationID.HUMAN_STAFF_BLOCK, AttackStyle.NON_ATTACK),
|
||||||
|
TAKING_HIT_UNARMED(AnimationID.HUMAN_UNARMEDBLOCK, AttackStyle.NON_ATTACK),
|
||||||
|
TAKING_HIT_VERACS_FLAIL(AnimationID.BARROW_GUTHAN_DEFEND, AttackStyle.NON_ATTACK),
|
||||||
|
TAKING_HIT_WHIP(AnimationID.SLAYER_ABYSSAL_WHIP_DEFEND, AttackStyle.NON_ATTACK),
|
||||||
|
TAKING_HIT_KISTEN(AnimationID.HUMAN_WEAPONS_CRIMSON_KISTEN_DEF, AttackStyle.NON_ATTACK),
|
||||||
|
TAKING_HIT_HALLOWFELL(AnimationID.HUMAN_HALLOWFELL_DEFEND, AttackStyle.NON_ATTACK),
|
||||||
|
LOW_ALCH(AnimationID.HUMAN_CASTLOWLVLALCHEMY, AttackStyle.NON_ATTACK),
|
||||||
|
HIGH_ALCH(AnimationID.HUMAN_CASTHIGHLVLALCHEMY, AttackStyle.NON_ATTACK);
|
||||||
|
|
||||||
|
private static final Map<Integer, AnimationData> DATA;
|
||||||
|
private static final Map<Spellbook, Set<AnimationData>> SPELL_BOOK_ANIMATIONS;
|
||||||
|
private static final Map<Integer, AnimationData> NOT_ATTACKS;
|
||||||
|
private static final Set<Integer> STANDARD_BOW_ATTACKS;
|
||||||
|
|
||||||
|
public final int animationId;
|
||||||
|
public final long metaData;
|
||||||
|
@Getter
|
||||||
|
public final AttackStyle attackStyle;
|
||||||
|
@Getter
|
||||||
|
private final Spellbook spellbook;
|
||||||
|
|
||||||
|
AnimationData(int animationId, AttackStyle attackStyle)
|
||||||
|
{
|
||||||
|
if (attackStyle == null)
|
||||||
|
{
|
||||||
|
throw new InvalidParameterException("Attack Style must be valid for AnimationData");
|
||||||
|
}
|
||||||
|
this.animationId = animationId;
|
||||||
|
this.attackStyle = attackStyle;
|
||||||
|
this.metaData = MetaData.NO_DATA.d;
|
||||||
|
this.spellbook = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimationData(int animationId, AttackStyle attackStyle, long data)
|
||||||
|
{
|
||||||
|
if (attackStyle == null)
|
||||||
|
{
|
||||||
|
throw new InvalidParameterException("Attack Style must be valid for AnimationData");
|
||||||
|
}
|
||||||
|
this.animationId = animationId;
|
||||||
|
this.attackStyle = attackStyle;
|
||||||
|
this.metaData = data;
|
||||||
|
this.spellbook = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimationData(int animationId, AttackStyle attackStyle, Spellbook book, long data)
|
||||||
|
{
|
||||||
|
if (attackStyle == null)
|
||||||
|
{
|
||||||
|
throw new InvalidParameterException("Attack Style must be valid for AnimationData");
|
||||||
|
}
|
||||||
|
this.animationId = animationId;
|
||||||
|
this.attackStyle = attackStyle;
|
||||||
|
this.metaData = data;
|
||||||
|
this.spellbook = book;
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
{
|
||||||
|
ImmutableMap.Builder<Integer, AnimationData> builder = new ImmutableMap.Builder<>();
|
||||||
|
ImmutableMap.Builder<Integer, AnimationData> notAttacksBuilder = new ImmutableMap.Builder<>();
|
||||||
|
Map<Spellbook, Set<AnimationData>> spellBookBuilder = new HashMap<>();
|
||||||
|
ImmutableSet.Builder<Integer> standardBowBuilder = new ImmutableSet.Builder<>();
|
||||||
|
|
||||||
|
for (Spellbook s : Spellbook.values())
|
||||||
|
{
|
||||||
|
spellBookBuilder.put(s, new HashSet<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (AnimationData data : values())
|
||||||
|
{
|
||||||
|
builder.put(data.animationId, data);
|
||||||
|
|
||||||
|
if (data.spellbook != null)
|
||||||
|
{
|
||||||
|
if (data.attackStyle != AttackStyle.MAGIC)
|
||||||
|
{
|
||||||
|
throw new InvalidParameterException("Spell book should only be magic animations");
|
||||||
|
}
|
||||||
|
spellBookBuilder.get(data.spellbook).add(data);
|
||||||
|
}
|
||||||
|
if (data.attackStyle == AttackStyle.NON_ATTACK)
|
||||||
|
{
|
||||||
|
notAttacksBuilder.put(data.animationId, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MetaData.hasFlagSet(data.metaData, MetaData.STANDARD_BOW_ATTACK))
|
||||||
|
{
|
||||||
|
standardBowBuilder.add(data.animationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DATA = builder.build();
|
||||||
|
NOT_ATTACKS = notAttacksBuilder.build();
|
||||||
|
SPELL_BOOK_ANIMATIONS = spellBookBuilder;
|
||||||
|
STANDARD_BOW_ATTACKS = standardBowBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AnimationData fromId(int animationId)
|
||||||
|
{
|
||||||
|
return DATA.get(animationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<AnimationData> getAnimationsForSpellbook(Spellbook s)
|
||||||
|
{
|
||||||
|
return SPELL_BOOK_ANIMATIONS.get(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isManualCasting(AnimationData animationData)
|
||||||
|
{
|
||||||
|
if (animationData != null && animationData.spellbook != null)
|
||||||
|
{
|
||||||
|
return animationData.attackStyle == AttackStyle.MAGIC &&
|
||||||
|
SPELL_BOOK_ANIMATIONS.get(animationData.spellbook).contains(animationData);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isBlockListAnimation(int animationId)
|
||||||
|
{
|
||||||
|
return NOT_ATTACKS.containsKey(animationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
String[] words = super.toString().toLowerCase().split("_");
|
||||||
|
Arrays.stream(words)
|
||||||
|
.map(StringUtils::capitalize).collect(Collectors.toList()).toArray(words);
|
||||||
|
|
||||||
|
return String.join(" ", words);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean matchesSpellbook(Spellbook s)
|
||||||
|
{
|
||||||
|
if (this.spellbook != null)
|
||||||
|
{
|
||||||
|
return this.spellbook == s;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isStandardBowAttack()
|
||||||
|
{
|
||||||
|
return STANDARD_BOW_ATTACKS.contains(this.animationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBlockListAnimation()
|
||||||
|
{
|
||||||
|
return NOT_ATTACKS.containsKey(this.animationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum AttackStyle
|
||||||
|
{
|
||||||
|
MELEE,
|
||||||
|
RANGED,
|
||||||
|
MAGIC,
|
||||||
|
NON_ATTACK;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return StringUtils.capitalize(super.toString().toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum MetaData
|
||||||
|
{
|
||||||
|
NO_DATA(),
|
||||||
|
SPECIAL_ATTACK(1),
|
||||||
|
STANDARD_BOW_ATTACK(2);
|
||||||
|
|
||||||
|
MetaData(int bitshift)
|
||||||
|
{
|
||||||
|
this.d = 1 << bitshift;
|
||||||
|
}
|
||||||
|
MetaData()
|
||||||
|
{
|
||||||
|
this.d = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean hasFlagSet(long input, MetaData data)
|
||||||
|
{
|
||||||
|
return (input & data.d) == data.d;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final long d;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return StringUtils.capitalize(super.toString().toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat;
|
||||||
|
|
||||||
|
public enum AttackProcedure
|
||||||
|
{
|
||||||
|
POWERED_STAVE,
|
||||||
|
MANUAL_AUTO_CAST,
|
||||||
|
MELEE_OR_RANGE
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.helpers.combat.variablespeed.VariableSpeed;
|
||||||
|
import com.example.essentialsplus.helpers.combat.variablespeed.state.TickCount;
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.Getter;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.events.FakeXpDrop;
|
||||||
|
import net.runelite.api.events.SoundEffectPlayed;
|
||||||
|
import net.runelite.api.events.StatChanged;
|
||||||
|
import net.runelite.api.gameval.ItemID;
|
||||||
|
import net.runelite.client.game.ItemManager;
|
||||||
|
|
||||||
|
public class AttackSpeed
|
||||||
|
{
|
||||||
|
private final ArrayDeque<Integer> specialPercentageEvents = new ArrayDeque<>();
|
||||||
|
private final Damage damage = new Damage();
|
||||||
|
@Getter
|
||||||
|
private int dmgDealt = -1;
|
||||||
|
private final TickCount tickCount;
|
||||||
|
private final VariableSpeed variableSpeed;
|
||||||
|
@Getter
|
||||||
|
private int soundEffectTick = -1;
|
||||||
|
@Getter
|
||||||
|
private int soundEffectId = -1;
|
||||||
|
@Getter
|
||||||
|
private boolean isUsingMagic = false;
|
||||||
|
|
||||||
|
public AttackSpeed(final TickCount tc, final VariableSpeed vs)
|
||||||
|
{
|
||||||
|
this.tickCount = tc;
|
||||||
|
this.variableSpeed = vs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int compute(final Client client, final AnimationData curAnimation, final Spellbook spellbook, final ItemManager itemManager)
|
||||||
|
{
|
||||||
|
final int weaponId = CombatUtils.getWeaponId(client);
|
||||||
|
final PoweredStaves stave = PoweredStaves.getPoweredStaves(weaponId, curAnimation);
|
||||||
|
final int specDelta = CombatUtils.getLastDelta(specialPercentageEvents);
|
||||||
|
dmgDealt = damage.compute(tickCount);
|
||||||
|
if (stave != null && stave.getAnimations() != null && stave.getAnimations().contains(curAnimation))
|
||||||
|
{
|
||||||
|
isUsingMagic = true;
|
||||||
|
return variableSpeed.compute(client, curAnimation, AttackProcedure.POWERED_STAVE, spellbook, dmgDealt, specDelta, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchesSpellbook(client, curAnimation, spellbook) && ManualCasting.is(client, curAnimation, soundEffectTick, soundEffectId))
|
||||||
|
{
|
||||||
|
isUsingMagic = true;
|
||||||
|
return variableSpeed.compute(client, curAnimation, AttackProcedure.MANUAL_AUTO_CAST, spellbook, dmgDealt, specDelta, getMagicBaseSpeed(weaponId));
|
||||||
|
}
|
||||||
|
|
||||||
|
isUsingMagic = false;
|
||||||
|
final int aspeed = CombatUtils.getWeaponSpeed(client, itemManager, weaponId);
|
||||||
|
return variableSpeed.compute(client, curAnimation, AttackProcedure.MELEE_OR_RANGE, spellbook, dmgDealt, specDelta, aspeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onTick()
|
||||||
|
{
|
||||||
|
while (specialPercentageEvents.size() > 5)
|
||||||
|
{
|
||||||
|
specialPercentageEvents.removeFirst();
|
||||||
|
}
|
||||||
|
damage.onTick();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void varbitSpecialAttackChanged(final int value)
|
||||||
|
{
|
||||||
|
specialPercentageEvents.addLast(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean onXpDrop(final FakeXpDrop event)
|
||||||
|
{
|
||||||
|
return damage.onXpDrop(event, tickCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean onXpDrop(final StatChanged event)
|
||||||
|
{
|
||||||
|
return damage.onXpDrop(event, tickCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onSoundEffectPlayed(final Client client, final SoundEffectPlayed event)
|
||||||
|
{
|
||||||
|
soundEffectTick = client.getTickCount();
|
||||||
|
soundEffectId = event.getSoundId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Map<Integer, Integer> NON_STANDARD_MAGIC_WEAPON_SPEEDS = new ImmutableMap.Builder<Integer, Integer>()
|
||||||
|
.put(ItemID.TWINFLAME_STAFF, 6)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private static int getMagicBaseSpeed(final int weaponId)
|
||||||
|
{
|
||||||
|
if (weaponId == ItemID.NIGHTMARE_STAFF_HARMONISED)
|
||||||
|
{
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
return NON_STANDARD_MAGIC_WEAPON_SPEEDS.getOrDefault(weaponId, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean matchesSpellbook(final Client client, final AnimationData curAnimation, final Spellbook currentSpellBook)
|
||||||
|
{
|
||||||
|
if (curAnimation != null && curAnimation.getAttackStyle() == AnimationData.AttackStyle.MAGIC && curAnimation.getSpellbook() == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (curAnimation != null && curAnimation.matchesSpellbook(currentSpellBook))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (client.getTickCount() == soundEffectTick)
|
||||||
|
{
|
||||||
|
return CastingSoundData.getSpellBookFromId(soundEffectId) == currentSpellBook;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset()
|
||||||
|
{
|
||||||
|
dmgDealt = -1;
|
||||||
|
soundEffectId = -1;
|
||||||
|
soundEffectTick = -1;
|
||||||
|
isUsingMagic = false;
|
||||||
|
specialPercentageEvents.clear();
|
||||||
|
damage.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import net.runelite.api.Skill;
|
||||||
|
|
||||||
|
public enum AttackStyle
|
||||||
|
{
|
||||||
|
ACCURATE("Accurate", Skill.ATTACK),
|
||||||
|
AGGRESSIVE("Aggressive", Skill.STRENGTH),
|
||||||
|
DEFENSIVE("Defensive", Skill.DEFENCE),
|
||||||
|
CONTROLLED("Controlled", Skill.ATTACK, Skill.STRENGTH, Skill.DEFENCE),
|
||||||
|
RANGING("Ranging", Skill.RANGED),
|
||||||
|
LONGRANGE("Longrange", Skill.RANGED, Skill.DEFENCE),
|
||||||
|
CASTING("Casting", Skill.MAGIC),
|
||||||
|
DEFENSIVE_CASTING("Defensive Casting", Skill.MAGIC, Skill.DEFENCE),
|
||||||
|
OTHER("Other");
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final String name;
|
||||||
|
@Getter
|
||||||
|
private final Skill[] skills;
|
||||||
|
|
||||||
|
AttackStyle(String name, Skill... skills)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
this.skills = skills;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat;
|
||||||
|
|
||||||
|
public enum AttackType
|
||||||
|
{
|
||||||
|
CRUSH,
|
||||||
|
SLASH,
|
||||||
|
STAB,
|
||||||
|
RANGED,
|
||||||
|
MAGIC,
|
||||||
|
NONE;
|
||||||
|
|
||||||
|
public boolean IsMelee()
|
||||||
|
{
|
||||||
|
return this.equals(CRUSH) || this.equals(SLASH) || this.equals(STAB);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Set;
|
||||||
|
import lombok.Getter;
|
||||||
|
import net.runelite.api.Actor;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.NPC;
|
||||||
|
import net.runelite.api.Player;
|
||||||
|
import net.runelite.api.gameval.NpcID;
|
||||||
|
import net.runelite.api.gameval.SpotanimID;
|
||||||
|
import net.runelite.client.game.NPCManager;
|
||||||
|
|
||||||
|
public class Attacking
|
||||||
|
{
|
||||||
|
private static final Set<AnimationData> UNRELIABLE_ANIMATIONS = new ImmutableSet.Builder<AnimationData>()
|
||||||
|
.add(AnimationData.RANGED_BLOWPIPE).add(AnimationData.RANGED_BLAZING_BLOWPIPE)
|
||||||
|
.add(AnimationData.MAGIC_EYE_OF_AYAK).add(AnimationData.MAGIC_EYE_OF_AYAK_SPEC).build();
|
||||||
|
|
||||||
|
private static final Set<Integer> SPECIAL_NPCS = new ImmutableSet.Builder<Integer>()
|
||||||
|
.add(NpcID.POH_COMBAT_DUMMY_UPGRADED_UNDEADSLAYER_NPC, NpcID.POH_COMBAT_DUMMY_UPGRADED_ETHER_NPC,
|
||||||
|
NpcID.POH_COMBAT_DUMMY_UPGRADED_KQ_NPC, NpcID.POH_COMBAT_DUMMY_UPGRADED_KURASK_NPC,
|
||||||
|
NpcID.POH_COMBAT_DUMMY_UPGRADED_VAMPIRE_NPC, NpcID.POH_COMBAT_DUMMY_UPGRADED_VORKATH_NPC,
|
||||||
|
NpcID.POH_COMBAT_DUMMY_NPC, NpcID.POH_COMBAT_DUMMY_UNDEADSLAYER_NPC,
|
||||||
|
NpcID.POH_COMBAT_DUMMY_UPGRADED_NPC, NpcID.NIGHTMARE_TOTEM_1_READY, NpcID.NIGHTMARE_TOTEM_2_READY,
|
||||||
|
NpcID.NIGHTMARE_TOTEM_3_READY, NpcID.NIGHTMARE_TOTEM_4_READY)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
public static boolean isPlayerAttacking(final Client client, final NPCManager npcManager)
|
||||||
|
{
|
||||||
|
final Player localPlayer = client.getLocalPlayer();
|
||||||
|
if (localPlayer == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final int animationId = localPlayer.getAnimation();
|
||||||
|
if (AnimationData.isBlockListAnimation(animationId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean notWalking = animationId != -1 || getSalamanderAttack(client);
|
||||||
|
|
||||||
|
final Actor target = localPlayer.getInteracting();
|
||||||
|
if (target instanceof NPC)
|
||||||
|
{
|
||||||
|
final NPC npc = (NPC) target;
|
||||||
|
final boolean containsAttackOption = npc.getComposition() != null
|
||||||
|
&& npc.getComposition().getActions() != null
|
||||||
|
&& Arrays.asList(npc.getComposition().getActions()).contains("Attack");
|
||||||
|
Integer health = null;
|
||||||
|
if (npcManager != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
health = npcManager.getHealth(npc.getId());
|
||||||
|
}
|
||||||
|
catch (Exception ignored)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final boolean hasHealthAndLevel = health != null && health > 0 && target.getCombatLevel() > 0;
|
||||||
|
final boolean attackingNPC = hasHealthAndLevel || SPECIAL_NPCS.contains(npc.getId()) || containsAttackOption;
|
||||||
|
return attackingNPC && notWalking;
|
||||||
|
}
|
||||||
|
if (target instanceof Player)
|
||||||
|
{
|
||||||
|
return notWalking;
|
||||||
|
}
|
||||||
|
if (target == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final AnimationData fromId = AnimationData.fromId(animationId);
|
||||||
|
if (UNRELIABLE_ANIMATIONS.contains(fromId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return fromId != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Attack playerAttack(final Client client)
|
||||||
|
{
|
||||||
|
final Player localPlayer = client.getLocalPlayer();
|
||||||
|
if (localPlayer == null)
|
||||||
|
{
|
||||||
|
return new Attack(-1, false, null);
|
||||||
|
}
|
||||||
|
final int animationId = localPlayer.getAnimation();
|
||||||
|
final Actor target = localPlayer.getInteracting();
|
||||||
|
return new Attack(animationId, getSalamanderAttack(client), target);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean getSalamanderAttack(final Client client)
|
||||||
|
{
|
||||||
|
Player localPlayer = client.getLocalPlayer();
|
||||||
|
return localPlayer != null && localPlayer.hasSpotAnim(SpotanimID.FIREBREATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Attack
|
||||||
|
{
|
||||||
|
@Getter
|
||||||
|
private final int animationId;
|
||||||
|
@Getter
|
||||||
|
private final boolean salamander;
|
||||||
|
@Getter
|
||||||
|
private final Actor target;
|
||||||
|
|
||||||
|
public Attack(final int animationId, final boolean salamander, final Actor target)
|
||||||
|
{
|
||||||
|
this.animationId = animationId;
|
||||||
|
this.salamander = salamander;
|
||||||
|
this.target = target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
|
||||||
|
public enum CastingSoundData
|
||||||
|
{
|
||||||
|
STANDARD_BIND(101, Spellbook.STANDARD),
|
||||||
|
STANDARD_CONFUSE(119, Spellbook.STANDARD),
|
||||||
|
STANDARD_CRUMBLE_UNDEAD(122, Spellbook.STANDARD),
|
||||||
|
STANDARD_CURSE(127, Spellbook.STANDARD),
|
||||||
|
STANDARD_EARTH_BLAST(128, Spellbook.STANDARD),
|
||||||
|
STANDARD_EARTH_BOLT(130, Spellbook.STANDARD),
|
||||||
|
STANDARD_EARTH_STRIKE(132, Spellbook.STANDARD),
|
||||||
|
STANDARD_EARTH_SURGE(4025, Spellbook.STANDARD),
|
||||||
|
STANDARD_EARTH_WAVE(134, Spellbook.STANDARD),
|
||||||
|
STANDARD_ENFEEBLE(148, Spellbook.STANDARD),
|
||||||
|
STANDARD_ENTANGLE(151, Spellbook.STANDARD),
|
||||||
|
STANDARD_FIRE_BLAST(155, Spellbook.STANDARD),
|
||||||
|
STANDARD_FIRE_BOLT(157, Spellbook.STANDARD),
|
||||||
|
STANDARD_FIRE_STRIKE(160, Spellbook.STANDARD),
|
||||||
|
STANDARD_FIRE_SURGE(4032, Spellbook.STANDARD),
|
||||||
|
STANDARD_IBANS_BLAST_FIRE_WAVE(162, Spellbook.STANDARD),
|
||||||
|
STANDARD_SLAYER_DART(1718, Spellbook.STANDARD),
|
||||||
|
STANDARD_SNARE(3003, Spellbook.STANDARD),
|
||||||
|
STANDARD_STUN(3004, Spellbook.STANDARD),
|
||||||
|
STANDARD_VULNERABILITY(3009, Spellbook.STANDARD),
|
||||||
|
STANDARD_WATER_BLAST(207, Spellbook.STANDARD),
|
||||||
|
STANDARD_WATER_BOLT(209, Spellbook.STANDARD),
|
||||||
|
STANDARD_WATER_STRIKE(221, Spellbook.STANDARD),
|
||||||
|
STANDARD_WATER_SURGE(4030, Spellbook.STANDARD),
|
||||||
|
STANDARD_WATER_WAVE(213, Spellbook.STANDARD),
|
||||||
|
STANDARD_WEAKEN(3011, Spellbook.STANDARD),
|
||||||
|
STANDARD_WIND_BLAST(216, Spellbook.STANDARD),
|
||||||
|
STANDARD_WIND_BOLT(218, Spellbook.STANDARD),
|
||||||
|
STANDARD_WIND_STRIKE(220, Spellbook.STANDARD),
|
||||||
|
STANDARD_WIND_SURGE(4028, Spellbook.STANDARD),
|
||||||
|
STANDARD_WIND_WAVE(222, Spellbook.STANDARD),
|
||||||
|
|
||||||
|
ANCIENT_BLOOD_SPELL(106, Spellbook.ANCIENT),
|
||||||
|
ANCIENT_ICE_SPELL(171, Spellbook.ANCIENT),
|
||||||
|
ANCIENT_SHADOW_SPELL(178, Spellbook.ANCIENT),
|
||||||
|
ANCIENT_SMOKE_SPELL(183, Spellbook.ANCIENT),
|
||||||
|
|
||||||
|
ARCEUUS_DARK_DEMONBANE(5053, Spellbook.ARCEUUS),
|
||||||
|
ARCEUUS_GHOSTLY_GRASP(5042, Spellbook.ARCEUUS),
|
||||||
|
ARCEUUS_INFERIOR_DEMONBANE(5038, Spellbook.ARCEUUS),
|
||||||
|
ARCEUUS_SKELETAL_GRASP(5026, Spellbook.ARCEUUS),
|
||||||
|
ARCEUUS_SUPERIOR_DEMONBANE(5027, Spellbook.ARCEUUS),
|
||||||
|
ARCEUUS_UNDEAD_GRASP(5030, Spellbook.ARCEUUS);
|
||||||
|
|
||||||
|
private final int id;
|
||||||
|
private final Spellbook spellbook;
|
||||||
|
|
||||||
|
CastingSoundData(int id, Spellbook b)
|
||||||
|
{
|
||||||
|
this.id = id;
|
||||||
|
this.spellbook = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final ImmutableMap<Integer, CastingSoundData> SOUNDS;
|
||||||
|
|
||||||
|
static
|
||||||
|
{
|
||||||
|
ImmutableMap.Builder<Integer, CastingSoundData> builder = new ImmutableMap.Builder<>();
|
||||||
|
for (CastingSoundData data : values())
|
||||||
|
{
|
||||||
|
builder.put(data.id, data);
|
||||||
|
}
|
||||||
|
SOUNDS = builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isCastingSound(int id)
|
||||||
|
{
|
||||||
|
return SOUNDS.containsKey(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Spellbook getSpellBookFromId(int id)
|
||||||
|
{
|
||||||
|
if (!SOUNDS.containsKey(id))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return SOUNDS.get(id).spellbook;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import net.runelite.api.Actor;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.EquipmentInventorySlot;
|
||||||
|
import net.runelite.api.Item;
|
||||||
|
import net.runelite.api.ItemContainer;
|
||||||
|
import net.runelite.api.NPC;
|
||||||
|
import net.runelite.api.WorldView;
|
||||||
|
import net.runelite.api.coords.LocalPoint;
|
||||||
|
import net.runelite.api.coords.WorldPoint;
|
||||||
|
import net.runelite.api.gameval.InventoryID;
|
||||||
|
import net.runelite.api.gameval.ItemID;
|
||||||
|
import net.runelite.api.gameval.VarPlayerID;
|
||||||
|
import net.runelite.api.gameval.VarbitID;
|
||||||
|
import net.runelite.client.game.ItemEquipmentStats;
|
||||||
|
import net.runelite.client.game.ItemManager;
|
||||||
|
import net.runelite.client.game.ItemStats;
|
||||||
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
|
|
||||||
|
public class CombatUtils
|
||||||
|
{
|
||||||
|
public static int getItemIdFromContainer(final ItemContainer container, final int slotID)
|
||||||
|
{
|
||||||
|
if (container == null)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
final Item item = container.getItem(slotID);
|
||||||
|
return (item != null) ? item.getId() : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getWeaponIdRaw(final Client client)
|
||||||
|
{
|
||||||
|
return getItemIdFromContainer(client.getItemContainer(InventoryID.WORN),
|
||||||
|
EquipmentInventorySlot.WEAPON.getSlotIdx());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WorldPoint getLocalLocation(final Client client)
|
||||||
|
{
|
||||||
|
final LocalPoint localPoint = client.getLocalPlayer().getLocalLocation();
|
||||||
|
return WorldPoint.fromLocalInstance(client, localPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AttackStyle getAttackStyle(final Client client)
|
||||||
|
{
|
||||||
|
final AttackStyle[] attackStyles = getWeaponType(client).getAttackStyles(client);
|
||||||
|
int currentAttackStyleVarbit = client.getVarpValue(VarPlayerID.COM_MODE);
|
||||||
|
final int castingMode = client.getVarbitValue(VarbitID.AUTOCAST_DEFMODE);
|
||||||
|
if (currentAttackStyleVarbit < attackStyles.length)
|
||||||
|
{
|
||||||
|
if (currentAttackStyleVarbit == 4)
|
||||||
|
{
|
||||||
|
currentAttackStyleVarbit += castingMode;
|
||||||
|
}
|
||||||
|
return attackStyles[currentAttackStyleVarbit];
|
||||||
|
}
|
||||||
|
return AttackStyle.ACCURATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WeaponType getWeaponType(final Client client)
|
||||||
|
{
|
||||||
|
final int currentEquippedWeaponTypeVarbit = client.getVarbitValue(VarbitID.COMBAT_WEAPON_CATEGORY);
|
||||||
|
return WeaponType.getWeaponType(currentEquippedWeaponTypeVarbit);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AttackType getAttackType(final Client client)
|
||||||
|
{
|
||||||
|
final WeaponType weaponType = getWeaponType(client);
|
||||||
|
final int currentAttackStyleVarbit = client.getVarpValue(VarPlayerID.COM_MODE);
|
||||||
|
if (currentAttackStyleVarbit < weaponType.getAttackTypes().length)
|
||||||
|
{
|
||||||
|
return weaponType.getAttackTypes()[currentAttackStyleVarbit];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getTargetId(final Client client)
|
||||||
|
{
|
||||||
|
final NPC target = getTargetNPC(client);
|
||||||
|
int targetId = 0;
|
||||||
|
if (target != null)
|
||||||
|
{
|
||||||
|
targetId = target.getId();
|
||||||
|
}
|
||||||
|
return targetId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NPC getTargetNPC(final Client client)
|
||||||
|
{
|
||||||
|
final Actor target = client.getLocalPlayer().getInteracting();
|
||||||
|
if (target != null && (target instanceof NPC))
|
||||||
|
{
|
||||||
|
return (NPC) target;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isInRegionId(final Client client, final int id)
|
||||||
|
{
|
||||||
|
final int[] regions = regions(client);
|
||||||
|
if (regions == null || regions.length == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return ArrayUtils.contains(regions, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isInRegionId(final Client client, final Set<Integer> ids)
|
||||||
|
{
|
||||||
|
final int[] regions = regions(client);
|
||||||
|
if (regions == null || regions.length == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (final int id : regions)
|
||||||
|
{
|
||||||
|
if (ids.contains(id))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int[] regions(final Client client)
|
||||||
|
{
|
||||||
|
final WorldView wv = client.getTopLevelWorldView();
|
||||||
|
if (wv == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return wv.getMapRegions();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getLastDelta(final ArrayDeque<Integer> events)
|
||||||
|
{
|
||||||
|
int i = 0, last = -1, secondLast = -1;
|
||||||
|
final var it = events.descendingIterator();
|
||||||
|
while (it.hasNext())
|
||||||
|
{
|
||||||
|
if (i == 0)
|
||||||
|
{
|
||||||
|
last = it.next();
|
||||||
|
}
|
||||||
|
else if (i == 1)
|
||||||
|
{
|
||||||
|
secondLast = it.next();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return last - secondLast;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Map<Integer, Integer> WEAPON_ID_MAPPING_WORKAROUNDS = new ImmutableMap.Builder<Integer, Integer>()
|
||||||
|
.put(ItemID.VENATOR_BOW_ORNAMENT, ItemID.VENATOR_BOW)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private static final Map<Integer, Integer> NON_STANDARD_ATTACK_SPEEDS = new ImmutableMap.Builder<Integer, Integer>()
|
||||||
|
.put(ItemID.HALLOWFELL, 6)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
public static int getWeaponId(final Client client)
|
||||||
|
{
|
||||||
|
final int weaponId = getWeaponIdRaw(client);
|
||||||
|
return WEAPON_ID_MAPPING_WORKAROUNDS.getOrDefault(weaponId, weaponId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ItemStats getWeaponStats(final Client client, final ItemManager itemManager, final int weaponId)
|
||||||
|
{
|
||||||
|
if (NON_STANDARD_ATTACK_SPEEDS.containsKey(weaponId))
|
||||||
|
{
|
||||||
|
return new ItemStats(true, -1, -1,
|
||||||
|
ItemEquipmentStats.builder().aspeed(NON_STANDARD_ATTACK_SPEEDS.get(weaponId)).build());
|
||||||
|
}
|
||||||
|
return itemManager.getItemStats(weaponId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getWeaponSpeed(final Client client, final ItemManager itemManager, final int weaponId)
|
||||||
|
{
|
||||||
|
final ItemStats weaponStats = getWeaponStats(client, itemManager, weaponId);
|
||||||
|
if (weaponStats == null || weaponStats.getEquipment() == null)
|
||||||
|
{
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
return weaponStats.getEquipment().getAspeed();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.helpers.combat.variablespeed.state.TickCount;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import net.runelite.api.Skill;
|
||||||
|
import net.runelite.api.events.FakeXpDrop;
|
||||||
|
import net.runelite.api.events.StatChanged;
|
||||||
|
|
||||||
|
public class Damage
|
||||||
|
{
|
||||||
|
private static final double MODIFIER = 1;
|
||||||
|
private static final double GLOBAL_MODIFIER = 1;
|
||||||
|
|
||||||
|
private final ArrayDeque<Integer> hpExpEarned = new ArrayDeque<>();
|
||||||
|
private final ArrayDeque<Integer> hpExpEarnedTickCount = new ArrayDeque<>();
|
||||||
|
|
||||||
|
public boolean onXpDrop(StatChanged event, TickCount tc)
|
||||||
|
{
|
||||||
|
if (event.getSkill() != Skill.HITPOINTS)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
hpExpEarnedTickCount.addLast(tc.get());
|
||||||
|
hpExpEarned.addLast(event.getXp());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean onXpDrop(FakeXpDrop event, TickCount tc)
|
||||||
|
{
|
||||||
|
if (event.getSkill() != Skill.HITPOINTS)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
hpExpEarnedTickCount.addLast(tc.get());
|
||||||
|
hpExpEarnedTickCount.addLast(tc.get());
|
||||||
|
hpExpEarned.addLast(0);
|
||||||
|
hpExpEarned.addLast(event.getXp());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int compute(TickCount tc)
|
||||||
|
{
|
||||||
|
if (hpExpEarnedTickCount.isEmpty())
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
final int lastTc = hpExpEarnedTickCount.getLast();
|
||||||
|
if (!tc.isWithinNTicks(lastTc, 1))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
final double xp = (double) CombatUtils.getLastDelta(hpExpEarned);
|
||||||
|
return (int) Math.round(xp * (3.0d / 4.0d) * MODIFIER * GLOBAL_MODIFIER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onTick()
|
||||||
|
{
|
||||||
|
while (hpExpEarnedTickCount.size() > 5)
|
||||||
|
{
|
||||||
|
hpExpEarnedTickCount.removeFirst();
|
||||||
|
}
|
||||||
|
while (hpExpEarned.size() > 5)
|
||||||
|
{
|
||||||
|
hpExpEarned.removeFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset()
|
||||||
|
{
|
||||||
|
hpExpEarned.clear();
|
||||||
|
hpExpEarnedTickCount.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat;
|
||||||
|
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
|
||||||
|
public class ManualCasting
|
||||||
|
{
|
||||||
|
public static boolean is(final Client client, final AnimationData curId, final int soundEffectTick, final int soundEffectId)
|
||||||
|
{
|
||||||
|
final boolean castingFromSound = client.getTickCount() == soundEffectTick
|
||||||
|
? CastingSoundData.isCastingSound(soundEffectId)
|
||||||
|
: false;
|
||||||
|
final boolean castingFromAnimation = AnimationData.isManualCasting(curId);
|
||||||
|
return castingFromSound || castingFromAnimation;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.Getter;
|
||||||
|
import net.runelite.api.gameval.ItemID;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
public enum PoweredStaves
|
||||||
|
{
|
||||||
|
WEAPON_ACCURSED(Set.of(AnimationData.MAGIC_STANDARD_WAVE_STAFF, AnimationData.MAGIC_STANDARD_WAVE_STAFF_WALK,
|
||||||
|
AnimationData.MAGIC_ACCURSED_SCEPTRE_SPEC), ItemID.WILD_CAVE_ACCURSED_CHARGED),
|
||||||
|
WEAPON_GAUNTLET_STAFF(Set.of(AnimationData.MAGIC_STANDARD_WAVE_STAFF, AnimationData.MAGIC_STANDARD_WAVE_STAFF_WALK),
|
||||||
|
ItemID.GAUNTLET_MAGIC_T1, ItemID.GAUNTLET_MAGIC_T2, ItemID.GAUNTLET_MAGIC_T3, ItemID.GAUNTLET_MAGIC_T1_HM,
|
||||||
|
ItemID.GAUNTLET_MAGIC_T2_HM, ItemID.GAUNTLET_MAGIC_T3_HM),
|
||||||
|
WEAPON_BONE_STAFF(AnimationData.MELEE_GENERIC_SLASH, ItemID.RAT_BONE_STAFF),
|
||||||
|
WEAPON_DAWNBRINGER(Set.of(AnimationData.MAGIC_STANDARD_WAVE_STAFF, AnimationData.MAGIC_STANDARD_WAVE_STAFF_WALK),
|
||||||
|
ItemID.VERZIK_SPECIAL_WEAPON),
|
||||||
|
WEAPON_HARM(Set.of(AnimationData.MAGIC_STANDARD_STRIKE_BOLT_BLAST_STAFF, AnimationData.MAGIC_STANDARD_WAVE_STAFF,
|
||||||
|
AnimationData.MAGIC_STANDARD_SURGE_STAFF, AnimationData.MAGIC_STANDARD_WAVE_STAFF_WALK,
|
||||||
|
AnimationData.MAGIC_STANDARD_STRIKE_BOLT_BLAST_STAFF_WALK, AnimationData.MAGIC_STANDARD_SURGE_STAFF_WALK),
|
||||||
|
ItemID.NIGHTMARE_STAFF_HARMONISED),
|
||||||
|
WEAPON_SANG(Set.of(AnimationData.MAGIC_STANDARD_WAVE_STAFF, AnimationData.MAGIC_STANDARD_WAVE_STAFF_WALK),
|
||||||
|
ItemID.SANGUINESTI_STAFF, ItemID.SANGUINESTI_STAFF_OR),
|
||||||
|
WEAPON_STARTER_STAFF(ItemID.DEADMAN_STARTER_STAFF, ItemID.DEADMAN_APOCALYPSE_STAFF),
|
||||||
|
WEAPON_SWAMP(Set.of(AnimationData.MAGIC_STANDARD_WAVE_STAFF, AnimationData.MAGIC_STANDARD_WAVE_STAFF_WALK),
|
||||||
|
ItemID.TOXIC_TOTS_CHARGED, ItemID.TOXIC_TOTS_I_CHARGED, ItemID.TOXIC_TOTS_CHARGED_ORN,
|
||||||
|
ItemID.TOXIC_TOTS_I_CHARGED_ORN),
|
||||||
|
WEAPON_THAMMARON(Set.of(AnimationData.MAGIC_STANDARD_WAVE_STAFF, AnimationData.MAGIC_STANDARD_WAVE_STAFF_WALK),
|
||||||
|
ItemID.WILD_CAVE_SCEPTRE_CHARGED),
|
||||||
|
WEAPON_TRIDENT(Set.of(AnimationData.MAGIC_STANDARD_WAVE_STAFF, AnimationData.MAGIC_STANDARD_WAVE_STAFF_WALK),
|
||||||
|
ItemID.TOTS, ItemID.TOTS_CHARGED, ItemID.TOTS_I_CHARGED, ItemID.TOTS_CHARGED_ORN, ItemID.TOTS_ORN,
|
||||||
|
ItemID.TOTS_I_CHARGED_ORN),
|
||||||
|
WEAPON_WARPED_SCEPTRE(AnimationData.MAGIC_WARPED_SCEPTRE, ItemID.WARPED_SCEPTRE);
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final Set<Integer> ids;
|
||||||
|
@Getter
|
||||||
|
private final Set<AnimationData> animations;
|
||||||
|
|
||||||
|
private static Set<Integer> ListToSet(int... id)
|
||||||
|
{
|
||||||
|
ImmutableSet.Builder<Integer> builder = new ImmutableSet.Builder<>();
|
||||||
|
if (id.length == 0)
|
||||||
|
{
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
for (int i : id)
|
||||||
|
{
|
||||||
|
builder.add(i);
|
||||||
|
}
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
PoweredStaves(int... id)
|
||||||
|
{
|
||||||
|
this.ids = ListToSet(id);
|
||||||
|
this.animations = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
PoweredStaves(AnimationData spell, int... id)
|
||||||
|
{
|
||||||
|
this.ids = ListToSet(id);
|
||||||
|
this.animations = Set.of(spell);
|
||||||
|
}
|
||||||
|
|
||||||
|
PoweredStaves(Set<AnimationData> spell, int... id)
|
||||||
|
{
|
||||||
|
this.ids = ListToSet(id);
|
||||||
|
this.animations = spell;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static final int UNKNOWN_SPELL = 0xDEADBEEF;
|
||||||
|
protected static final ImmutableMap<Integer, ImmutableMap<Integer, PoweredStaves>> POWERED_STAVES;
|
||||||
|
|
||||||
|
static
|
||||||
|
{
|
||||||
|
ImmutableMap.Builder<Integer, ImmutableMap<Integer, PoweredStaves>> builder = new ImmutableMap.Builder<>();
|
||||||
|
|
||||||
|
for (PoweredStaves p : values())
|
||||||
|
{
|
||||||
|
for (int id : p.ids)
|
||||||
|
{
|
||||||
|
ImmutableMap.Builder<Integer, PoweredStaves> spellMap = new ImmutableMap.Builder<>();
|
||||||
|
if (p.animations == null)
|
||||||
|
{
|
||||||
|
spellMap.put(UNKNOWN_SPELL, p);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (AnimationData spell : p.animations)
|
||||||
|
{
|
||||||
|
spellMap.put(spell.animationId, p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.put(id, spellMap.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
POWERED_STAVES = builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PoweredStaves getPoweredStaves(int weaponId, AnimationData animation)
|
||||||
|
{
|
||||||
|
ImmutableMap<Integer, PoweredStaves> weaponMap = POWERED_STAVES.get(weaponId);
|
||||||
|
if (weaponMap == null || animation == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (weaponMap.containsKey(UNKNOWN_SPELL))
|
||||||
|
{
|
||||||
|
return weaponMap.get(UNKNOWN_SPELL);
|
||||||
|
}
|
||||||
|
return weaponMap.get(animation.animationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
String[] words = super.toString().toLowerCase().split("_");
|
||||||
|
Arrays.stream(words).map(StringUtils::capitalize).collect(Collectors.toList()).toArray(words);
|
||||||
|
return String.join(" ", words);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
|
||||||
|
public enum Spellbook
|
||||||
|
{
|
||||||
|
STANDARD(0),
|
||||||
|
ANCIENT(1),
|
||||||
|
LUNAR(2),
|
||||||
|
ARCEUUS(3);
|
||||||
|
|
||||||
|
private final int id;
|
||||||
|
|
||||||
|
Spellbook(int id)
|
||||||
|
{
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final ImmutableMap<Integer, Spellbook> BOOKS;
|
||||||
|
|
||||||
|
static
|
||||||
|
{
|
||||||
|
ImmutableMap.Builder<Integer, Spellbook> builder = new ImmutableMap.Builder<>();
|
||||||
|
for (Spellbook data : values())
|
||||||
|
{
|
||||||
|
builder.put(data.id, data);
|
||||||
|
}
|
||||||
|
BOOKS = builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Spellbook fromVarbit(int varbit)
|
||||||
|
{
|
||||||
|
return BOOKS.get(varbit);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat;
|
||||||
|
|
||||||
|
import static com.example.essentialsplus.helpers.combat.AttackStyle.ACCURATE;
|
||||||
|
import static com.example.essentialsplus.helpers.combat.AttackStyle.AGGRESSIVE;
|
||||||
|
import static com.example.essentialsplus.helpers.combat.AttackStyle.CASTING;
|
||||||
|
import static com.example.essentialsplus.helpers.combat.AttackStyle.DEFENSIVE;
|
||||||
|
import static com.example.essentialsplus.helpers.combat.AttackStyle.DEFENSIVE_CASTING;
|
||||||
|
import static com.example.essentialsplus.helpers.combat.AttackStyle.OTHER;
|
||||||
|
import static com.example.essentialsplus.helpers.combat.AttackType.CRUSH;
|
||||||
|
import static com.example.essentialsplus.helpers.combat.AttackType.MAGIC;
|
||||||
|
import static com.example.essentialsplus.helpers.combat.AttackType.NONE;
|
||||||
|
import static com.example.essentialsplus.helpers.combat.AttackType.RANGED;
|
||||||
|
import static com.example.essentialsplus.helpers.combat.AttackType.SLASH;
|
||||||
|
import static com.example.essentialsplus.helpers.combat.AttackType.STAB;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.Getter;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.EnumID;
|
||||||
|
import net.runelite.api.ParamID;
|
||||||
|
import net.runelite.api.StructComposition;
|
||||||
|
|
||||||
|
public enum WeaponType
|
||||||
|
{
|
||||||
|
UNARMED(CRUSH, CRUSH, null, CRUSH),
|
||||||
|
AXE(SLASH, SLASH, CRUSH, SLASH),
|
||||||
|
BLUNT(CRUSH, CRUSH, null, CRUSH),
|
||||||
|
BOW(RANGED, RANGED, null, RANGED),
|
||||||
|
CLAW(SLASH, SLASH, STAB, SLASH),
|
||||||
|
HEAVY_RANGED(RANGED, RANGED, null, RANGED),
|
||||||
|
SALAMANDER(SLASH, RANGED, MAGIC, null),
|
||||||
|
CHINCHOMPA(RANGED, RANGED, null, RANGED),
|
||||||
|
GUN(NONE, CRUSH, null, null),
|
||||||
|
SLASH_SWORD(SLASH, SLASH, STAB, SLASH),
|
||||||
|
TWO_HANDED_SWORD(SLASH, SLASH, CRUSH, SLASH),
|
||||||
|
PICKAXE(STAB, STAB, CRUSH, STAB),
|
||||||
|
POLEARM(STAB, SLASH, null, STAB),
|
||||||
|
POLESTAFF(CRUSH, CRUSH, null, CRUSH),
|
||||||
|
SCYTHE(SLASH, SLASH, CRUSH, SLASH),
|
||||||
|
SPEAR(STAB, SLASH, CRUSH, STAB),
|
||||||
|
SPIKED(CRUSH, CRUSH, STAB, CRUSH),
|
||||||
|
STAB_SWORD(STAB, STAB, SLASH, STAB),
|
||||||
|
STAFF(CRUSH, CRUSH, null, CRUSH, MAGIC, MAGIC),
|
||||||
|
THROWN(RANGED, RANGED, null, RANGED),
|
||||||
|
WHIP(SLASH, SLASH, null, SLASH),
|
||||||
|
BLADED_STAFF(STAB, SLASH, null, CRUSH, MAGIC, MAGIC),
|
||||||
|
TWO_HANDED_SWORD_GODSWORD(STAB, SLASH, null, CRUSH, MAGIC, MAGIC),
|
||||||
|
POWERED_STAFF(SLASH, SLASH, CRUSH, SLASH),
|
||||||
|
BANNER(MAGIC, MAGIC, null, MAGIC),
|
||||||
|
POLEARM_CRYSTAL_HALBERD(STAB, SLASH, CRUSH, STAB),
|
||||||
|
BLUDGEON(STAB, SLASH, null, STAB),
|
||||||
|
BULWARK(CRUSH, CRUSH, null, CRUSH),
|
||||||
|
POWERED_WAND(CRUSH, null, null, NONE),
|
||||||
|
PARTISAN(MAGIC, MAGIC, null, MAGIC),
|
||||||
|
PARTISAN_2(STAB, STAB, CRUSH, STAB),
|
||||||
|
PLACE_HOLDER_0(null, null, null),
|
||||||
|
PLACE_HOLDER_1(null, null, null),
|
||||||
|
PLACE_HOLDER_2(null, null, null),
|
||||||
|
PLACE_HOLDER_3(null, null, null),
|
||||||
|
FLAIL(SLASH, SLASH, null, SLASH);
|
||||||
|
|
||||||
|
private static final Map<Integer, WeaponType> WEAPON_TYPES;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final AttackType[] attackTypes;
|
||||||
|
|
||||||
|
static
|
||||||
|
{
|
||||||
|
ImmutableMap.Builder<Integer, WeaponType> builder = new ImmutableMap.Builder<>();
|
||||||
|
for (WeaponType weaponType : values())
|
||||||
|
{
|
||||||
|
builder.put(weaponType.ordinal(), weaponType);
|
||||||
|
}
|
||||||
|
WEAPON_TYPES = builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
WeaponType(AttackType... attackTypes)
|
||||||
|
{
|
||||||
|
this.attackTypes = attackTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AttackStyle[] getAttackStyles(Client client)
|
||||||
|
{
|
||||||
|
int weaponType = this.ordinal();
|
||||||
|
int weaponStyleEnum = client.getEnum(EnumID.WEAPON_STYLES).getIntValue(weaponType);
|
||||||
|
if (weaponStyleEnum == -1)
|
||||||
|
{
|
||||||
|
if (weaponType == 22)
|
||||||
|
{
|
||||||
|
return new AttackStyle[]{
|
||||||
|
ACCURATE, AGGRESSIVE, null, DEFENSIVE, CASTING, DEFENSIVE_CASTING,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (weaponType == 30)
|
||||||
|
{
|
||||||
|
return new AttackStyle[]{
|
||||||
|
ACCURATE, AGGRESSIVE, AGGRESSIVE, DEFENSIVE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return new AttackStyle[0];
|
||||||
|
}
|
||||||
|
int[] weaponStyleStructs = client.getEnum(weaponStyleEnum).getIntVals();
|
||||||
|
|
||||||
|
AttackStyle[] styles = new AttackStyle[weaponStyleStructs.length];
|
||||||
|
int i = 0;
|
||||||
|
for (int style : weaponStyleStructs)
|
||||||
|
{
|
||||||
|
StructComposition attackStyleStruct = client.getStructComposition(style);
|
||||||
|
String attackStyleName = attackStyleStruct.getStringValue(ParamID.ATTACK_STYLE_NAME);
|
||||||
|
AttackStyle attackStyle = AttackStyle.valueOf(attackStyleName.toUpperCase());
|
||||||
|
if (attackStyle == OTHER)
|
||||||
|
{
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i == 5 && attackStyle == DEFENSIVE)
|
||||||
|
{
|
||||||
|
attackStyle = DEFENSIVE_CASTING;
|
||||||
|
}
|
||||||
|
|
||||||
|
styles[i++] = attackStyle;
|
||||||
|
}
|
||||||
|
return styles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WeaponType getWeaponType(int id)
|
||||||
|
{
|
||||||
|
return WEAPON_TYPES.get(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat.variablespeed;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.helpers.combat.AnimationData;
|
||||||
|
import com.example.essentialsplus.helpers.combat.AttackProcedure;
|
||||||
|
import com.example.essentialsplus.helpers.combat.AttackType;
|
||||||
|
import com.example.essentialsplus.helpers.combat.CombatUtils;
|
||||||
|
import com.example.essentialsplus.helpers.combat.Spellbook;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.coords.WorldPoint;
|
||||||
|
import net.runelite.api.gameval.NpcID;
|
||||||
|
|
||||||
|
public class Amoxliatl implements IVariableSpeed
|
||||||
|
{
|
||||||
|
private static final int AMOXLIATL_REGION_ID = 5446;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkType,
|
||||||
|
final Spellbook spellbook, final int damageDealt, final int lastSpecDelta, final int baseSpeed,
|
||||||
|
final int curSpeed)
|
||||||
|
{
|
||||||
|
final WorldPoint location = CombatUtils.getLocalLocation(client);
|
||||||
|
final int targetId = CombatUtils.getTargetId(client);
|
||||||
|
final AttackType attkType = CombatUtils.getAttackType(client);
|
||||||
|
if (location.getRegionID() == AMOXLIATL_REGION_ID && targetId == NpcID.AMOXLIATL_ICE_BLOCK && attkType != null && attkType.IsMelee())
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return curSpeed;
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat.variablespeed;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.helpers.combat.AnimationData;
|
||||||
|
import com.example.essentialsplus.helpers.combat.AttackProcedure;
|
||||||
|
import com.example.essentialsplus.helpers.combat.Spellbook;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.gameval.SpotanimID;
|
||||||
|
|
||||||
|
public class BloodMoonSet implements IVariableSpeed
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkType,
|
||||||
|
final Spellbook spellbook, final int damageDealt, final int lastSpecDelta, final int baseSpeed,
|
||||||
|
final int curSpeed)
|
||||||
|
{
|
||||||
|
if (client.getLocalPlayer() != null && client.getLocalPlayer().hasSpotAnim(SpotanimID.SPECIAL_DUAL_MACUAHUITL_SPOTANIM))
|
||||||
|
{
|
||||||
|
return curSpeed - 1;
|
||||||
|
}
|
||||||
|
return curSpeed;
|
||||||
|
}
|
||||||
|
}
|
||||||
+127
@@ -0,0 +1,127 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat.variablespeed;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.helpers.combat.AnimationData;
|
||||||
|
import com.example.essentialsplus.helpers.combat.AttackProcedure;
|
||||||
|
import com.example.essentialsplus.helpers.combat.Attacking;
|
||||||
|
import com.example.essentialsplus.helpers.combat.CombatUtils;
|
||||||
|
import com.example.essentialsplus.helpers.combat.Spellbook;
|
||||||
|
import com.example.essentialsplus.helpers.combat.variablespeed.state.TickCount;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.NPC;
|
||||||
|
import net.runelite.api.gameval.AnimationID;
|
||||||
|
import net.runelite.api.gameval.ItemID;
|
||||||
|
import net.runelite.api.gameval.NpcID;
|
||||||
|
import net.runelite.client.game.ItemManager;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class DoomOfMokhaiotl implements IVariableSpeed
|
||||||
|
{
|
||||||
|
private static final Set<Integer> DOOM_REGION_IDS = new ImmutableSet.Builder<Integer>()
|
||||||
|
.add(5269)
|
||||||
|
.add(13668)
|
||||||
|
.add(14180)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private static final Set<Integer> COOLDOWN_OVERWRITES_IDS = new ImmutableSet.Builder<Integer>()
|
||||||
|
.add(NpcID.DOM_DEMONIC_ENERGY)
|
||||||
|
.add(NpcID.DOM_DEMONIC_ENERGY_GIANT_MAGE)
|
||||||
|
.add(NpcID.DOM_DEMONIC_ENERGY_GIANT_RANGE)
|
||||||
|
.add(NpcID.DOM_DEMONIC_ENERGY_MAGE)
|
||||||
|
.add(NpcID.DOM_DEMONIC_ENERGY_RANGE)
|
||||||
|
.add(NpcID.DOM_DEMONIC_ENERGY_MELEE)
|
||||||
|
.add(NpcID.DOM_SHOCKWAVE_PATH_NODE)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private static final Set<Integer> NO_COOLDOWN_WEAPON = new ImmutableSet.Builder<Integer>()
|
||||||
|
.add(ItemID.SILVERLIGHT)
|
||||||
|
.add(ItemID.DARKLIGHT)
|
||||||
|
.add(ItemID.ARCLIGHT)
|
||||||
|
.add(ItemID.EMBERLIGHT)
|
||||||
|
.add(ItemID.BONE_CLAWS)
|
||||||
|
.add(ItemID.SCORCHING_BOW)
|
||||||
|
.add(ItemID.HOLY_WATER)
|
||||||
|
.add(ItemID.EYE_OF_AYAK)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private final TickCount tickCount;
|
||||||
|
private int larvaeConsumed = -1;
|
||||||
|
private int shieldConsumed = -1;
|
||||||
|
|
||||||
|
public DoomOfMokhaiotl(final TickCount tc)
|
||||||
|
{
|
||||||
|
this.tickCount = tc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean onRender(final Client client, final ItemManager itemManager, final Spellbook spellbook)
|
||||||
|
{
|
||||||
|
if (!CombatUtils.isInRegionId(client, DOOM_REGION_IDS))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final var atk = Attacking.playerAttack(client);
|
||||||
|
final AnimationData anim = AnimationData.fromId(atk.getAnimationId());
|
||||||
|
if (anim == null || atk.getTarget() == null || !(atk.getTarget() instanceof NPC) || anim.isBlockListAnimation())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final NPC npc = (NPC) atk.getTarget();
|
||||||
|
final int npcId = npc.getId();
|
||||||
|
if (COOLDOWN_OVERWRITES_IDS.contains(npcId))
|
||||||
|
{
|
||||||
|
if (tickCount.isWithinNTicks(larvaeConsumed, 1))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final int weaponId = CombatUtils.getWeaponId(client);
|
||||||
|
final boolean isDemonbaneSpell = spellbook == Spellbook.ARCEUUS && AnimationData.isManualCasting(anim) && anim == AnimationData.MAGIC_ARCEUUS_DEMONBANE;
|
||||||
|
if (NO_COOLDOWN_WEAPON.contains(weaponId) || isDemonbaneSpell)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
larvaeConsumed = tickCount.get();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (npcId == NpcID.DOM_BOSS)
|
||||||
|
{
|
||||||
|
if (tickCount.isWithinNTicks(shieldConsumed, 30))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final int animId = npc.getAnimation();
|
||||||
|
if (animId == AnimationID.DOM_BEAM_CHARGE_LOOP || animId == AnimationID.DOM_BEAM_CHARGE)
|
||||||
|
{
|
||||||
|
if (CombatUtils.getAttackType(client) != null && CombatUtils.getAttackType(client).IsMelee())
|
||||||
|
{
|
||||||
|
shieldConsumed = tickCount.get();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkType,
|
||||||
|
final Spellbook spellbook, final int damageDealt, final int lastSpecDelta, final int baseSpeed,
|
||||||
|
final int curSpeed)
|
||||||
|
{
|
||||||
|
final int targetId = CombatUtils.getTargetId(client);
|
||||||
|
final boolean inDoom = CombatUtils.isInRegionId(client, DOOM_REGION_IDS);
|
||||||
|
if (inDoom && COOLDOWN_OVERWRITES_IDS.contains(targetId))
|
||||||
|
{
|
||||||
|
final int weaponId = CombatUtils.getWeaponId(client);
|
||||||
|
final boolean isDemonbaneSpell = spellbook == Spellbook.ARCEUUS && AnimationData.isManualCasting(curAnimation) && curAnimation == AnimationData.MAGIC_ARCEUUS_DEMONBANE;
|
||||||
|
if (NO_COOLDOWN_WEAPON.contains(weaponId) || isDemonbaneSpell)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return curSpeed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat.variablespeed;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.helpers.combat.AnimationData;
|
||||||
|
import com.example.essentialsplus.helpers.combat.AttackProcedure;
|
||||||
|
import com.example.essentialsplus.helpers.combat.Spellbook;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
|
||||||
|
public class EyeOfAyak implements IVariableSpeed
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkType,
|
||||||
|
final Spellbook spellbook, final int damageDealt, final int lastSpecDelta, final int baseSpeed,
|
||||||
|
final int curSpeed)
|
||||||
|
{
|
||||||
|
if (curAnimation == AnimationData.MAGIC_EYE_OF_AYAK_SPEC)
|
||||||
|
{
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
return curSpeed;
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat.variablespeed;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.helpers.combat.AnimationData;
|
||||||
|
import com.example.essentialsplus.helpers.combat.AttackProcedure;
|
||||||
|
import com.example.essentialsplus.helpers.combat.Spellbook;
|
||||||
|
import com.example.essentialsplus.helpers.combat.variablespeed.state.IStateTracker;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
|
||||||
|
public interface IVariableSpeed extends IStateTracker
|
||||||
|
{
|
||||||
|
int apply(
|
||||||
|
final Client client,
|
||||||
|
final AnimationData curAnimation,
|
||||||
|
final AttackProcedure atkType,
|
||||||
|
final Spellbook spellbook,
|
||||||
|
final int damageDealt,
|
||||||
|
final int lastSpecDelta,
|
||||||
|
final int baseSpeed,
|
||||||
|
final int curSpeed
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat.variablespeed;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.helpers.combat.AnimationData;
|
||||||
|
import com.example.essentialsplus.helpers.combat.Attacking;
|
||||||
|
import com.example.essentialsplus.helpers.combat.CombatUtils;
|
||||||
|
import com.example.essentialsplus.helpers.combat.variablespeed.state.TickCount;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.NPC;
|
||||||
|
import net.runelite.api.gameval.AnimationID;
|
||||||
|
import net.runelite.api.gameval.NpcID;
|
||||||
|
import net.runelite.client.game.ItemManager;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class MaggotKing
|
||||||
|
{
|
||||||
|
private static final int MAGGOT_KING_REGION_ID = 11645;
|
||||||
|
|
||||||
|
private final TickCount tickCount;
|
||||||
|
private int consumed = -1;
|
||||||
|
|
||||||
|
public MaggotKing(final TickCount tc)
|
||||||
|
{
|
||||||
|
this.tickCount = tc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean onRender(final Client client, final ItemManager itemManager)
|
||||||
|
{
|
||||||
|
if (!CombatUtils.isInRegionId(client, MAGGOT_KING_REGION_ID) || tickCount.isWithinNTicks(consumed, 1))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final var atk = Attacking.playerAttack(client);
|
||||||
|
final AnimationData anim = AnimationData.fromId(atk.getAnimationId());
|
||||||
|
if (anim == null || atk.getTarget() == null || !(atk.getTarget() instanceof NPC))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final NPC npc = (NPC) atk.getTarget();
|
||||||
|
if (npc.getId() != NpcID.UR_MAGGOT_LARVAE || !anim.isStandardBowAttack()
|
||||||
|
|| npc.getAnimation() != AnimationID.UR_MAGGOT_LARVAE_FLY)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
consumed = tickCount.get();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int onChatMessage(final Client client, final String message)
|
||||||
|
{
|
||||||
|
if (message == null)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (message.contains("The sticky acid hampers your ability to attack!"))
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
else if (message.contains("The Maggot King's screech disrupts your concentration!"))
|
||||||
|
{
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat.variablespeed;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.helpers.combat.AnimationData;
|
||||||
|
import com.example.essentialsplus.helpers.combat.AttackProcedure;
|
||||||
|
import com.example.essentialsplus.helpers.combat.CombatUtils;
|
||||||
|
import com.example.essentialsplus.helpers.combat.Spellbook;
|
||||||
|
import com.example.essentialsplus.helpers.combat.variablespeed.state.Yama;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.NPC;
|
||||||
|
import net.runelite.api.gameval.ItemID;
|
||||||
|
|
||||||
|
public class PurgingStaffSpec implements IVariableSpeed
|
||||||
|
{
|
||||||
|
private final Yama yama;
|
||||||
|
public NPC lastTarget;
|
||||||
|
|
||||||
|
public PurgingStaffSpec(Yama yama)
|
||||||
|
{
|
||||||
|
this.yama = yama;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkType,
|
||||||
|
final Spellbook spellbook, final int damageDealt, final int lastSpecDelta, final int baseSpeed,
|
||||||
|
final int curSpeed)
|
||||||
|
{
|
||||||
|
if (this.yama == null || !this.yama.inYamaRegion)
|
||||||
|
{
|
||||||
|
return curSpeed;
|
||||||
|
}
|
||||||
|
final NPC target = CombatUtils.getTargetNPC(client);
|
||||||
|
final NPC flare = Yama.isEitherVoidFlare(target, lastTarget);
|
||||||
|
lastTarget = target;
|
||||||
|
if (flare == null)
|
||||||
|
{
|
||||||
|
return curSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
yama.dealVoidFlareDamage(flare, damageDealt);
|
||||||
|
if (lastSpecDelta != -250 || CombatUtils.getWeaponId(client) != ItemID.PURGING_STAFF || spellbook != Spellbook.ARCEUUS)
|
||||||
|
{
|
||||||
|
return curSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (yama.getVoidFlareDead(flare))
|
||||||
|
{
|
||||||
|
return curSpeed - 2;
|
||||||
|
}
|
||||||
|
return curSpeed;
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat.variablespeed;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.helpers.combat.AnimationData;
|
||||||
|
import com.example.essentialsplus.helpers.combat.AttackProcedure;
|
||||||
|
import com.example.essentialsplus.helpers.combat.AttackStyle;
|
||||||
|
import com.example.essentialsplus.helpers.combat.CombatUtils;
|
||||||
|
import com.example.essentialsplus.helpers.combat.Spellbook;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.gameval.VarPlayerID;
|
||||||
|
|
||||||
|
public class RapidAttackStyle implements IVariableSpeed
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkType,
|
||||||
|
final Spellbook spellbook, final int damageDealt, final int lastSpecDelta, final int baseSpeed,
|
||||||
|
final int curSpeed)
|
||||||
|
{
|
||||||
|
final boolean isRapid = client.getVarpValue(VarPlayerID.COM_MODE) == 1;
|
||||||
|
if (atkType == AttackProcedure.MELEE_OR_RANGE && CombatUtils.getAttackStyle(client) == AttackStyle.RANGING && isRapid)
|
||||||
|
{
|
||||||
|
return curSpeed - 1;
|
||||||
|
}
|
||||||
|
return curSpeed;
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat.variablespeed;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.helpers.combat.AnimationData;
|
||||||
|
import com.example.essentialsplus.helpers.combat.AttackProcedure;
|
||||||
|
import com.example.essentialsplus.helpers.combat.Spellbook;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
|
||||||
|
public class RedKerisSpec implements IVariableSpeed
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkType,
|
||||||
|
final Spellbook spellbook, final int damageDealt, final int lastSpecDelta, final int baseSpeed,
|
||||||
|
final int curSpeed)
|
||||||
|
{
|
||||||
|
if (lastSpecDelta != -750 || curAnimation != AnimationData.MELEE_RED_KERIS_SPEC)
|
||||||
|
{
|
||||||
|
return curSpeed;
|
||||||
|
}
|
||||||
|
if (damageDealt > 0)
|
||||||
|
{
|
||||||
|
return curSpeed + 4;
|
||||||
|
}
|
||||||
|
return curSpeed;
|
||||||
|
}
|
||||||
|
}
|
||||||
+174
@@ -0,0 +1,174 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat.variablespeed;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.helpers.combat.AnimationData;
|
||||||
|
import com.example.essentialsplus.helpers.combat.AttackProcedure;
|
||||||
|
import com.example.essentialsplus.helpers.combat.CombatUtils;
|
||||||
|
import com.example.essentialsplus.helpers.combat.Spellbook;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.NPC;
|
||||||
|
import net.runelite.api.coords.WorldPoint;
|
||||||
|
import net.runelite.api.events.GameTick;
|
||||||
|
import net.runelite.api.events.NpcDespawned;
|
||||||
|
import net.runelite.api.events.NpcSpawned;
|
||||||
|
import net.runelite.api.gameval.ItemID;
|
||||||
|
import net.runelite.api.gameval.NpcID;
|
||||||
|
|
||||||
|
public class RoyalTitans implements IVariableSpeed
|
||||||
|
{
|
||||||
|
private static final int ROYAL_TITANS_REGION_ID = 11669;
|
||||||
|
private static final int HP_FUDGE = 1;
|
||||||
|
private static final int ELEMENTAL_HP = 40 - HP_FUDGE;
|
||||||
|
|
||||||
|
private final Set<NPC> iceElementals = new HashSet<>();
|
||||||
|
private final Set<NPC> fireElementals = new HashSet<>();
|
||||||
|
private boolean removeDead = false;
|
||||||
|
|
||||||
|
private static final Set<AnimationData> ONE_SHOT_SPELLS = new ImmutableSet.Builder<AnimationData>()
|
||||||
|
.add(AnimationData.MAGIC_STANDARD_STRIKE_BOLT_BLAST)
|
||||||
|
.add(AnimationData.MAGIC_STANDARD_STRIKE_BOLT_BLAST_STAFF)
|
||||||
|
.add(AnimationData.MAGIC_STANDARD_STRIKE_BOLT_BLAST_STAFF_WALK)
|
||||||
|
.add(AnimationData.MAGIC_STANDARD_STRIKE_BOLT_BLAST_WALK)
|
||||||
|
.add(AnimationData.MAGIC_STANDARD_SURGE)
|
||||||
|
.add(AnimationData.MAGIC_STANDARD_SURGE_STAFF)
|
||||||
|
.add(AnimationData.MAGIC_STANDARD_SURGE_STAFF_WALK)
|
||||||
|
.add(AnimationData.MAGIC_STANDARD_WAVE)
|
||||||
|
.add(AnimationData.MAGIC_STANDARD_WAVE_STAFF)
|
||||||
|
.add(AnimationData.MAGIC_STANDARD_WAVE_STAFF_WALK)
|
||||||
|
.add(AnimationData.MAGIC_STANDARD_WAVE_WALK)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkType,
|
||||||
|
final Spellbook spellbook, final int damageDealt, final int lastSpecDelta, final int baseSpeed,
|
||||||
|
final int curSpeed)
|
||||||
|
{
|
||||||
|
if (earlyExit(client, atkType, damageDealt, spellbook))
|
||||||
|
{
|
||||||
|
return curSpeed;
|
||||||
|
}
|
||||||
|
final int targetId = CombatUtils.getTargetId(client);
|
||||||
|
if (spellbook != Spellbook.STANDARD || !isElemental(targetId) || !ONE_SHOT_SPELLS.contains(curAnimation))
|
||||||
|
{
|
||||||
|
return curSpeed;
|
||||||
|
}
|
||||||
|
final NPC target = CombatUtils.getTargetNPC(client);
|
||||||
|
if (target == null)
|
||||||
|
{
|
||||||
|
return curSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean wieldingTwinflame = CombatUtils.getWeaponId(client) == ItemID.TWINFLAME_STAFF;
|
||||||
|
final int computedDamage = wieldingTwinflame ? damageDealt + ((damageDealt * 4) / 10) : damageDealt;
|
||||||
|
|
||||||
|
if (computedDamage < ELEMENTAL_HP)
|
||||||
|
{
|
||||||
|
return curSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
final var set = targetId == NpcID.RT_SUMMON_ELEMENTAL_FIRE ? fireElementals : iceElementals;
|
||||||
|
int count = 1;
|
||||||
|
final var reference = target.getWorldLocation();
|
||||||
|
for (final NPC elemental : set)
|
||||||
|
{
|
||||||
|
if (elemental == target)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final WorldPoint worldLocation = elemental.getWorldLocation();
|
||||||
|
final int distanceTo2D = reference.distanceTo2D(worldLocation);
|
||||||
|
if (distanceTo2D <= 1)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean isTwinflameKillOnSecondProjectile = wieldingTwinflame &&
|
||||||
|
curAnimation == AnimationData.MAGIC_STANDARD_STRIKE_BOLT_BLAST_STAFF;
|
||||||
|
final int extraHitOffset = isTwinflameKillOnSecondProjectile ? 1 : 0;
|
||||||
|
|
||||||
|
final WorldPoint playerLoc = client.getLocalPlayer().getWorldLocation();
|
||||||
|
final int distance = playerLoc.distanceTo2D(reference);
|
||||||
|
final int hitDelay = 2 + (distance / 3) + extraHitOffset;
|
||||||
|
|
||||||
|
final int remainingAtImpact = Math.max(0, curSpeed - hitDelay);
|
||||||
|
final int remainingAfterReduction = Math.max(0, remainingAtImpact - count);
|
||||||
|
final int finalSpeed = Math.min(curSpeed, hitDelay + remainingAfterReduction);
|
||||||
|
|
||||||
|
removeDead = true;
|
||||||
|
return finalSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean earlyExit(final Client client, final AttackProcedure atkType, final int damageDealt,
|
||||||
|
final Spellbook spellbook)
|
||||||
|
{
|
||||||
|
return notInRegion(client)
|
||||||
|
|| atkType != AttackProcedure.MANUAL_AUTO_CAST
|
||||||
|
|| damageDealt <= 0
|
||||||
|
|| spellbook != Spellbook.STANDARD;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onGameTick(Client client, GameTick tick)
|
||||||
|
{
|
||||||
|
if (removeDead)
|
||||||
|
{
|
||||||
|
boolean removed = iceElementals.removeIf(NPC::isDead);
|
||||||
|
removed |= fireElementals.removeIf(NPC::isDead);
|
||||||
|
if (removed)
|
||||||
|
{
|
||||||
|
removeDead = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNpcSpawned(final Client client, final NpcSpawned npcSpawned)
|
||||||
|
{
|
||||||
|
if (notInRegion(client))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final NPC npc = npcSpawned.getNpc();
|
||||||
|
final int id = npc.getId();
|
||||||
|
if (id == NpcID.RT_SUMMON_ELEMENTAL_ICE)
|
||||||
|
{
|
||||||
|
iceElementals.add(npc);
|
||||||
|
}
|
||||||
|
else if (id == NpcID.RT_SUMMON_ELEMENTAL_FIRE)
|
||||||
|
{
|
||||||
|
fireElementals.add(npc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNpcDespawned(final Client client, final NpcDespawned npcDespawned)
|
||||||
|
{
|
||||||
|
if (notInRegion(client))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final NPC npc = npcDespawned.getNpc();
|
||||||
|
final int id = npc.getId();
|
||||||
|
if (id == NpcID.RT_SUMMON_ELEMENTAL_ICE)
|
||||||
|
{
|
||||||
|
iceElementals.remove(npc);
|
||||||
|
}
|
||||||
|
else if (id == NpcID.RT_SUMMON_ELEMENTAL_FIRE)
|
||||||
|
{
|
||||||
|
fireElementals.remove(npc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isElemental(int id)
|
||||||
|
{
|
||||||
|
return id == NpcID.RT_SUMMON_ELEMENTAL_FIRE || id == NpcID.RT_SUMMON_ELEMENTAL_ICE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean notInRegion(final Client client)
|
||||||
|
{
|
||||||
|
return CombatUtils.getLocalLocation(client).getRegionID() != ROYAL_TITANS_REGION_ID;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat.variablespeed;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.helpers.combat.AnimationData;
|
||||||
|
import com.example.essentialsplus.helpers.combat.AttackProcedure;
|
||||||
|
import com.example.essentialsplus.helpers.combat.CombatUtils;
|
||||||
|
import com.example.essentialsplus.helpers.combat.Spellbook;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.coords.WorldPoint;
|
||||||
|
import net.runelite.api.gameval.ItemID;
|
||||||
|
import net.runelite.api.gameval.NpcID;
|
||||||
|
|
||||||
|
public class Scurrius implements IVariableSpeed
|
||||||
|
{
|
||||||
|
private static final int SCURRIUS_REGION_ID = 13210;
|
||||||
|
private static final int SCURRIUS_MIN_X = 3290;
|
||||||
|
private static final int SCURRIUS_MAX_X = 3307;
|
||||||
|
private static final int SCURRIUS_MIN_Y = 9859;
|
||||||
|
private static final int SCURRIUS_MAX_Y = 9876;
|
||||||
|
|
||||||
|
private static boolean attackingGiantRatWithBoneWeapon(final int equipped, final int regionId, final int x,
|
||||||
|
final int y, final int target)
|
||||||
|
{
|
||||||
|
final boolean correctWeapon = equipped == ItemID.RAT_BONE_STAFF || equipped == ItemID.RAT_BONE_MACE || equipped == ItemID.RAT_BONE_BOW;
|
||||||
|
final boolean correctCoords = x >= SCURRIUS_MIN_X && x <= SCURRIUS_MAX_X && y >= SCURRIUS_MIN_Y && y <= SCURRIUS_MAX_Y;
|
||||||
|
final boolean correctRegion = regionId == SCURRIUS_REGION_ID;
|
||||||
|
final boolean correctEnemy = target == NpcID.RAT_BOSS_GIANT_RAT;
|
||||||
|
return correctWeapon && correctCoords && correctRegion && correctEnemy;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkType,
|
||||||
|
final Spellbook spellbook, final int damageDealt, final int lastSpecDelta, final int baseSpeed,
|
||||||
|
final int curSpeed)
|
||||||
|
{
|
||||||
|
final WorldPoint location = CombatUtils.getLocalLocation(client);
|
||||||
|
final int weaponId = CombatUtils.getWeaponId(client);
|
||||||
|
final int targetId = CombatUtils.getTargetId(client);
|
||||||
|
if (attackingGiantRatWithBoneWeapon(weaponId, location.getRegionID(), location.getX(), location.getY(), targetId))
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return curSpeed;
|
||||||
|
}
|
||||||
|
}
|
||||||
+257
@@ -0,0 +1,257 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat.variablespeed;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.helpers.combat.variablespeed.state.MarkOfDarkness;
|
||||||
|
import com.example.essentialsplus.helpers.combat.variablespeed.state.TickCount;
|
||||||
|
import com.example.essentialsplus.helpers.combat.variablespeed.state.Yama;
|
||||||
|
import com.example.essentialsplus.helpers.combat.variablespeed.state.YamaPhase;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.GraphicsObject;
|
||||||
|
import net.runelite.api.Player;
|
||||||
|
import net.runelite.api.WorldView;
|
||||||
|
import net.runelite.api.coords.LocalPoint;
|
||||||
|
import net.runelite.api.coords.WorldPoint;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class ShadowCrash
|
||||||
|
{
|
||||||
|
private static final int FIREBALL_ID = 3262;
|
||||||
|
private static final int TILE = 128;
|
||||||
|
|
||||||
|
private static final int HV_GAP_1 = 3;
|
||||||
|
private static final int HV_GAP_2 = 6;
|
||||||
|
private static final int HV_DELTA_1 = HV_GAP_1 * TILE;
|
||||||
|
private static final int HV_DELTA_2 = HV_GAP_2 * TILE;
|
||||||
|
private static final int D_GAP_1 = 2;
|
||||||
|
private static final int D_GAP_2 = 4;
|
||||||
|
private static final int D_DELTA_1 = D_GAP_1 * TILE;
|
||||||
|
private static final int D_DELTA_2 = D_GAP_2 * TILE;
|
||||||
|
|
||||||
|
private final TickCount tickCount;
|
||||||
|
private final Yama yama;
|
||||||
|
private final MarkOfDarkness mod;
|
||||||
|
|
||||||
|
private int consumed = -1;
|
||||||
|
private final Map<LocalPoint, GraphicsObject> fireballs;
|
||||||
|
private ArrayList<FireballLine> lines;
|
||||||
|
private WorldPoint player;
|
||||||
|
|
||||||
|
public ShadowCrash(Yama yama, MarkOfDarkness mod, TickCount tc)
|
||||||
|
{
|
||||||
|
this.yama = yama;
|
||||||
|
this.mod = mod;
|
||||||
|
this.tickCount = tc;
|
||||||
|
this.fireballs = new HashMap<>();
|
||||||
|
this.lines = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int onRender(final Client client, final int attackDelayHoldoffTicks, final boolean isUsingMagic)
|
||||||
|
{
|
||||||
|
if (yama == null || !yama.inYamaRegion || yama.phase() != YamaPhase.P3 || !mod.isActive())
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (!tickCount.isWithinNTicks(consumed, 2) && shadowCrashSweetSpot(client))
|
||||||
|
{
|
||||||
|
consumed = tickCount.get();
|
||||||
|
if (attackDelayHoldoffTicks <= 1 && isUsingMagic)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shadowCrashSweetSpot(final Client client)
|
||||||
|
{
|
||||||
|
getState(client);
|
||||||
|
if (lines.isEmpty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var line : lines)
|
||||||
|
{
|
||||||
|
if (line.isActive())
|
||||||
|
{
|
||||||
|
return line.inSweetSpot(client, player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getState(final Client client)
|
||||||
|
{
|
||||||
|
fireballs.clear();
|
||||||
|
lines.clear();
|
||||||
|
final WorldView tlwv = client.getTopLevelWorldView();
|
||||||
|
final Player localPlayer = client.getLocalPlayer();
|
||||||
|
if (localPlayer == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final WorldView playerWv = localPlayer.getWorldView();
|
||||||
|
if (tlwv != null)
|
||||||
|
{
|
||||||
|
getFireballs(tlwv);
|
||||||
|
}
|
||||||
|
if (playerWv != null && playerWv != tlwv)
|
||||||
|
{
|
||||||
|
getFireballs(playerWv);
|
||||||
|
}
|
||||||
|
if (fireballs.size() < 3)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
player = localPlayer.getWorldLocation();
|
||||||
|
lines = getLines();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getFireballs(final WorldView wv)
|
||||||
|
{
|
||||||
|
for (GraphicsObject graphicsObject : wv.getGraphicsObjects())
|
||||||
|
{
|
||||||
|
if (graphicsObject.getId() == FIREBALL_ID)
|
||||||
|
{
|
||||||
|
fireballs.put(graphicsObject.getLocation(), graphicsObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ArrayList<FireballLine> getLines()
|
||||||
|
{
|
||||||
|
final var result = new ArrayList<FireballLine>();
|
||||||
|
|
||||||
|
while (fireballs.size() >= 3)
|
||||||
|
{
|
||||||
|
LocalPoint first = null;
|
||||||
|
final Set<LocalPoint> fireballsLocations = fireballs.keySet();
|
||||||
|
for (var location : fireballsLocations)
|
||||||
|
{
|
||||||
|
if (first == null)
|
||||||
|
{
|
||||||
|
first = location;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (first.getX() > location.getX())
|
||||||
|
{
|
||||||
|
first = location;
|
||||||
|
}
|
||||||
|
else if (first.getX() == location.getX())
|
||||||
|
{
|
||||||
|
if (first.getY() > location.getY())
|
||||||
|
{
|
||||||
|
first = location;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (first == null)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
var centre = first.plus(D_DELTA_1, D_DELTA_1);
|
||||||
|
var end = first.plus(D_DELTA_2, D_DELTA_2);
|
||||||
|
if (fireballsLocations.contains(centre) && fireballsLocations.contains(end))
|
||||||
|
{
|
||||||
|
result.add(new FireballLine(LineType.D2, centre, fireballs.get(centre)));
|
||||||
|
removeLine(first, centre, end);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
centre = first.plus(HV_DELTA_1, 0);
|
||||||
|
end = first.plus(HV_DELTA_2, 0);
|
||||||
|
if (fireballsLocations.contains(centre) && fireballsLocations.contains(end))
|
||||||
|
{
|
||||||
|
result.add(new FireballLine(LineType.H, centre, fireballs.get(centre)));
|
||||||
|
removeLine(first, centre, end);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
centre = first.plus(0, HV_DELTA_1);
|
||||||
|
end = first.plus(0, HV_DELTA_2);
|
||||||
|
if (fireballsLocations.contains(centre) && fireballsLocations.contains(end))
|
||||||
|
{
|
||||||
|
result.add(new FireballLine(LineType.V, centre, fireballs.get(centre)));
|
||||||
|
removeLine(first, centre, end);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
centre = first.plus(D_DELTA_1, -D_DELTA_1);
|
||||||
|
end = first.plus(D_DELTA_2, -D_DELTA_2);
|
||||||
|
if (fireballsLocations.contains(centre) && fireballsLocations.contains(end))
|
||||||
|
{
|
||||||
|
result.add(new FireballLine(LineType.D1, centre, fireballs.get(centre)));
|
||||||
|
removeLine(first, centre, end);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeLine(final LocalPoint a, final LocalPoint b, final LocalPoint c)
|
||||||
|
{
|
||||||
|
fireballs.remove(a);
|
||||||
|
fireballs.remove(b);
|
||||||
|
fireballs.remove(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean eq(final WorldPoint t, final int dx, final int dy, final WorldPoint other)
|
||||||
|
{
|
||||||
|
if (t == null || other == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return t.getX() + dx == other.getX() && t.getY() + dy == other.getY();
|
||||||
|
}
|
||||||
|
|
||||||
|
static class FireballLine
|
||||||
|
{
|
||||||
|
private final LineType type;
|
||||||
|
private final LocalPoint centre;
|
||||||
|
private final GraphicsObject cObject;
|
||||||
|
|
||||||
|
FireballLine(final LineType type, final LocalPoint centre, final GraphicsObject cObject)
|
||||||
|
{
|
||||||
|
this.centre = centre;
|
||||||
|
this.cObject = cObject;
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isActive()
|
||||||
|
{
|
||||||
|
final int animationFrame = cObject.getAnimationFrame();
|
||||||
|
return animationFrame >= 30 && animationFrame <= 40;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean inSweetSpot(final Client client, final WorldPoint lp)
|
||||||
|
{
|
||||||
|
final WorldPoint wp = WorldPoint.fromLocal(client, centre);
|
||||||
|
switch (this.type)
|
||||||
|
{
|
||||||
|
case V:
|
||||||
|
return eq(wp, 1, 0, lp) || eq(wp, -1, 0, lp);
|
||||||
|
case H:
|
||||||
|
return eq(wp, 0, 1, lp) || eq(wp, 0, -1, lp);
|
||||||
|
case D1:
|
||||||
|
return eq(wp, 1, 1, lp) || eq(wp, -1, -1, lp);
|
||||||
|
case D2:
|
||||||
|
return eq(wp, -1, 1, lp) || eq(wp, 1, -1, lp);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LineType
|
||||||
|
{
|
||||||
|
V, H, D1, D2
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat.variablespeed;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.helpers.combat.AnimationData;
|
||||||
|
import com.example.essentialsplus.helpers.combat.AttackProcedure;
|
||||||
|
import com.example.essentialsplus.helpers.combat.AttackType;
|
||||||
|
import com.example.essentialsplus.helpers.combat.CombatUtils;
|
||||||
|
import com.example.essentialsplus.helpers.combat.Spellbook;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.gameval.NpcID;
|
||||||
|
|
||||||
|
public class TombsOfAmascut implements IVariableSpeed
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkType,
|
||||||
|
final Spellbook spellbook, final int damageDealt, final int lastSpecDelta, final int baseSpeed,
|
||||||
|
final int curSpeed)
|
||||||
|
{
|
||||||
|
final int targetId = CombatUtils.getTargetId(client);
|
||||||
|
final AttackType attkType = CombatUtils.getAttackType(client);
|
||||||
|
if (targetId == NpcID.WARDENS_P3_ORB_BLUE && attkType != null && attkType.IsMelee())
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return curSpeed;
|
||||||
|
}
|
||||||
|
}
|
||||||
+184
@@ -0,0 +1,184 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat.variablespeed;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.helpers.combat.AnimationData;
|
||||||
|
import com.example.essentialsplus.helpers.combat.AttackProcedure;
|
||||||
|
import com.example.essentialsplus.helpers.combat.AttackType;
|
||||||
|
import com.example.essentialsplus.helpers.combat.CombatUtils;
|
||||||
|
import com.example.essentialsplus.helpers.combat.Spellbook;
|
||||||
|
import com.example.essentialsplus.helpers.combat.WeaponType;
|
||||||
|
import com.example.essentialsplus.helpers.combat.variablespeed.state.TickCount;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.NPC;
|
||||||
|
import net.runelite.api.events.GameTick;
|
||||||
|
import net.runelite.api.events.NpcDespawned;
|
||||||
|
import net.runelite.api.events.NpcSpawned;
|
||||||
|
import net.runelite.api.gameval.NpcID;
|
||||||
|
import net.runelite.api.gameval.SpotanimID;
|
||||||
|
|
||||||
|
public class TormentedDemons implements IVariableSpeed
|
||||||
|
{
|
||||||
|
private final TickCount tickCount;
|
||||||
|
private final Map<NPC, DemonData> tormentedDemons = new HashMap<>();
|
||||||
|
|
||||||
|
public TormentedDemons(TickCount tc)
|
||||||
|
{
|
||||||
|
this.tickCount = tc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkType,
|
||||||
|
final Spellbook spellbook, final int damageDealt, final int lastSpecDelta, final int baseSpeed,
|
||||||
|
final int curSpeed)
|
||||||
|
{
|
||||||
|
final int targetId = CombatUtils.getTargetId(client);
|
||||||
|
if (!isTormentedDemon(targetId))
|
||||||
|
{
|
||||||
|
return curSpeed;
|
||||||
|
}
|
||||||
|
final NPC target = CombatUtils.getTargetNPC(client);
|
||||||
|
if (!tormentedDemons.containsKey(target))
|
||||||
|
{
|
||||||
|
return curSpeed;
|
||||||
|
}
|
||||||
|
final DemonData targetDemon = tormentedDemons.get(target);
|
||||||
|
if (!targetDemon.isVulnerable(tickCount.get()))
|
||||||
|
{
|
||||||
|
return curSpeed;
|
||||||
|
}
|
||||||
|
if (targetDemon.vulnConsumed(tickCount.get()))
|
||||||
|
{
|
||||||
|
return curSpeed;
|
||||||
|
}
|
||||||
|
switch (atkType)
|
||||||
|
{
|
||||||
|
case POWERED_STAVE:
|
||||||
|
return curSpeed;
|
||||||
|
case MANUAL_AUTO_CAST:
|
||||||
|
break;
|
||||||
|
case MELEE_OR_RANGE:
|
||||||
|
final WeaponType wep = CombatUtils.getWeaponType(client);
|
||||||
|
final AttackType attkType = CombatUtils.getAttackType(client);
|
||||||
|
if (wep != WeaponType.HEAVY_RANGED && (attkType == null || attkType != AttackType.CRUSH))
|
||||||
|
{
|
||||||
|
return curSpeed;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (curSpeed > 4)
|
||||||
|
{
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
return curSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isTormentedDemon(int targetId)
|
||||||
|
{
|
||||||
|
return targetId == NpcID.TORMENTED_DEMON_1 || targetId == NpcID.TORMENTED_DEMON_2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onGameTick(Client client, GameTick tick)
|
||||||
|
{
|
||||||
|
for (final Map.Entry<NPC, DemonData> td : tormentedDemons.entrySet())
|
||||||
|
{
|
||||||
|
final boolean isVulnerable = td.getKey().hasSpotAnim(SpotanimID.LUC2_UNDEAD_DEMON_EXPLOSION_FIRE_SPOT);
|
||||||
|
td.getValue().update(tickCount.get(), isVulnerable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNpcSpawned(final Client client, final NpcSpawned npcSpawned)
|
||||||
|
{
|
||||||
|
final NPC npc = npcSpawned.getNpc();
|
||||||
|
maybeInsertDemon(npc);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void maybeInsertDemon(final NPC npc)
|
||||||
|
{
|
||||||
|
if (isTormentedDemon(npc.getId()))
|
||||||
|
{
|
||||||
|
final boolean isVulnerable = npc.hasSpotAnim(SpotanimID.LUC2_UNDEAD_DEMON_EXPLOSION_FIRE_SPOT);
|
||||||
|
tormentedDemons.put(npc, new DemonData(tickCount.get(), isVulnerable));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNpcDespawned(final Client client, final NpcDespawned npcDespawned)
|
||||||
|
{
|
||||||
|
final NPC npc = npcDespawned.getNpc();
|
||||||
|
if (isTormentedDemon(npc.getId()))
|
||||||
|
{
|
||||||
|
tormentedDemons.remove(npc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class DemonData
|
||||||
|
{
|
||||||
|
private static final int VULN_TICKS_AFTER_END = 10;
|
||||||
|
private Integer vulnerableStart;
|
||||||
|
private Integer vulnerableFinish;
|
||||||
|
private int attacked;
|
||||||
|
|
||||||
|
DemonData(int tick, boolean vuln)
|
||||||
|
{
|
||||||
|
this.update(tick, vuln);
|
||||||
|
this.attacked = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void update(int tick, boolean vuln)
|
||||||
|
{
|
||||||
|
if (vuln && this.vulnerableStart == null)
|
||||||
|
{
|
||||||
|
this.vulnerableStart = tick;
|
||||||
|
this.vulnerableFinish = tick;
|
||||||
|
}
|
||||||
|
else if (vuln && this.vulnerableStart != null)
|
||||||
|
{
|
||||||
|
this.vulnerableFinish = tick;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!vuln)
|
||||||
|
{
|
||||||
|
this.vulnerableStart = null;
|
||||||
|
}
|
||||||
|
if (this.attacked != -1 && this.attacked + 1 >= tick)
|
||||||
|
{
|
||||||
|
this.attacked = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isVulnerable(int tick)
|
||||||
|
{
|
||||||
|
if (this.vulnerableFinish == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (this.vulnerableFinish + VULN_TICKS_AFTER_END) > tick;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean vulnConsumed(int tick)
|
||||||
|
{
|
||||||
|
if (this.attacked == tick || this.attacked == -1)
|
||||||
|
{
|
||||||
|
this.consumeVuln(tick);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (this.attacked > tick + VULN_TICKS_AFTER_END)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.consumeVuln(tick);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void consumeVuln(int tick)
|
||||||
|
{
|
||||||
|
this.attacked = tick;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat.variablespeed;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.helpers.combat.AnimationData;
|
||||||
|
import com.example.essentialsplus.helpers.combat.AttackProcedure;
|
||||||
|
import com.example.essentialsplus.helpers.combat.Spellbook;
|
||||||
|
import com.example.essentialsplus.helpers.combat.variablespeed.state.IStateTracker;
|
||||||
|
import com.example.essentialsplus.helpers.combat.variablespeed.state.MarkOfDarkness;
|
||||||
|
import com.example.essentialsplus.helpers.combat.variablespeed.state.TickCount;
|
||||||
|
import com.example.essentialsplus.helpers.combat.variablespeed.state.Yama;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.events.ChatMessage;
|
||||||
|
import net.runelite.api.events.GameTick;
|
||||||
|
import net.runelite.api.events.NpcDespawned;
|
||||||
|
import net.runelite.api.events.NpcSpawned;
|
||||||
|
|
||||||
|
public class VariableSpeed
|
||||||
|
{
|
||||||
|
private final TickCount tickCount;
|
||||||
|
private final Yama yama;
|
||||||
|
private final MarkOfDarkness markOfDarkness;
|
||||||
|
|
||||||
|
public final ShadowCrash shadowCrash;
|
||||||
|
public final MaggotKing maggotKing;
|
||||||
|
public final DoomOfMokhaiotl doomOfMokhaiotl;
|
||||||
|
|
||||||
|
private final IStateTracker[] toTrack;
|
||||||
|
private final IVariableSpeed[] toApply;
|
||||||
|
|
||||||
|
public VariableSpeed(TickCount tc)
|
||||||
|
{
|
||||||
|
this.tickCount = tc;
|
||||||
|
this.yama = new Yama();
|
||||||
|
this.markOfDarkness = new MarkOfDarkness();
|
||||||
|
this.shadowCrash = new ShadowCrash(yama, markOfDarkness, tc);
|
||||||
|
this.maggotKing = new MaggotKing(tc);
|
||||||
|
this.doomOfMokhaiotl = new DoomOfMokhaiotl(tc);
|
||||||
|
|
||||||
|
this.toTrack = new IStateTracker[]{
|
||||||
|
tc,
|
||||||
|
yama,
|
||||||
|
markOfDarkness
|
||||||
|
};
|
||||||
|
|
||||||
|
this.toApply = new IVariableSpeed[]{
|
||||||
|
new BloodMoonSet(),
|
||||||
|
new RapidAttackStyle(),
|
||||||
|
new RedKerisSpec(),
|
||||||
|
new PurgingStaffSpec(yama),
|
||||||
|
new EyeOfAyak(),
|
||||||
|
new TormentedDemons(tc),
|
||||||
|
new RoyalTitans(),
|
||||||
|
new Amoxliatl(),
|
||||||
|
new Scurrius(),
|
||||||
|
new TombsOfAmascut(),
|
||||||
|
doomOfMokhaiotl
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public int compute(final Client client, final AnimationData curAnimation, final AttackProcedure atkType,
|
||||||
|
final Spellbook spellbook, final int damageDealt, final int lastSpecDelta, final int baseSpeed)
|
||||||
|
{
|
||||||
|
int newSpeed = baseSpeed;
|
||||||
|
for (final IVariableSpeed i : toApply)
|
||||||
|
{
|
||||||
|
newSpeed = i.apply(client, curAnimation, atkType, spellbook, damageDealt, lastSpecDelta, baseSpeed, newSpeed);
|
||||||
|
}
|
||||||
|
return newSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onGameTick(final Client client, final GameTick tick)
|
||||||
|
{
|
||||||
|
for (final IStateTracker i : toTrack)
|
||||||
|
{
|
||||||
|
i.onGameTick(client, tick);
|
||||||
|
}
|
||||||
|
for (final IStateTracker i : toApply)
|
||||||
|
{
|
||||||
|
i.onGameTick(client, tick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onChatMessage(final Client client, final ChatMessage event)
|
||||||
|
{
|
||||||
|
for (final IStateTracker i : toTrack)
|
||||||
|
{
|
||||||
|
i.onChatMessage(client, event);
|
||||||
|
}
|
||||||
|
for (final IStateTracker i : toApply)
|
||||||
|
{
|
||||||
|
i.onChatMessage(client, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onNpcSpawned(final Client client, final NpcSpawned npcSpawned)
|
||||||
|
{
|
||||||
|
for (final IStateTracker i : toTrack)
|
||||||
|
{
|
||||||
|
i.onNpcSpawned(client, npcSpawned);
|
||||||
|
}
|
||||||
|
for (final IStateTracker i : toApply)
|
||||||
|
{
|
||||||
|
i.onNpcSpawned(client, npcSpawned);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onNpcDespawned(final Client client, final NpcDespawned npcDespawned)
|
||||||
|
{
|
||||||
|
for (final IStateTracker i : toTrack)
|
||||||
|
{
|
||||||
|
i.onNpcDespawned(client, npcDespawned);
|
||||||
|
}
|
||||||
|
for (final IStateTracker i : toApply)
|
||||||
|
{
|
||||||
|
i.onNpcDespawned(client, npcDespawned);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat.variablespeed.state;
|
||||||
|
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.events.ChatMessage;
|
||||||
|
import net.runelite.api.events.GameTick;
|
||||||
|
import net.runelite.api.events.NpcDespawned;
|
||||||
|
import net.runelite.api.events.NpcSpawned;
|
||||||
|
|
||||||
|
public interface IStateTracker
|
||||||
|
{
|
||||||
|
default void onGameTick(final Client client, final GameTick tick)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
default void onChatMessage(final Client client, final ChatMessage event)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
default void onNpcSpawned(final Client client, final NpcSpawned npcSpawned)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
default void onNpcDespawned(final Client client, final NpcDespawned npcDespawned)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat.variablespeed.state;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.helpers.combat.CombatUtils;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.Skill;
|
||||||
|
import net.runelite.api.events.ChatMessage;
|
||||||
|
import net.runelite.api.gameval.ItemID;
|
||||||
|
import net.runelite.client.util.RSTimeUnit;
|
||||||
|
|
||||||
|
public class MarkOfDarkness implements IStateTracker
|
||||||
|
{
|
||||||
|
private static final String MARK_OF_DARKNESS_MESSAGE = "You have placed a Mark of Darkness upon yourself.</col>";
|
||||||
|
|
||||||
|
private Instant modStartTime = Instant.now();
|
||||||
|
private Instant modEndTime = Instant.now();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onChatMessage(Client client, ChatMessage event)
|
||||||
|
{
|
||||||
|
final String message = event.getMessage();
|
||||||
|
if (message != null && message.endsWith(MARK_OF_DARKNESS_MESSAGE))
|
||||||
|
{
|
||||||
|
final Duration duration = getMarkOfDarknessDuration(client);
|
||||||
|
this.modStartTime = Instant.now();
|
||||||
|
this.modEndTime = modStartTime.plus(duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isActive()
|
||||||
|
{
|
||||||
|
final Instant n = Instant.now();
|
||||||
|
return this.modStartTime.isBefore(n) && this.modEndTime.isAfter(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Duration getMarkOfDarknessDuration(Client client)
|
||||||
|
{
|
||||||
|
final int magicLevel = client.getRealSkillLevel(Skill.MAGIC);
|
||||||
|
final Duration markOfDarknessDuration = Duration.of((long) magicLevel * 3, RSTimeUnit.GAME_TICKS);
|
||||||
|
if (CombatUtils.getWeaponId(client) == ItemID.PURGING_STAFF)
|
||||||
|
{
|
||||||
|
return markOfDarknessDuration.multipliedBy(5);
|
||||||
|
}
|
||||||
|
return markOfDarknessDuration;
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat.variablespeed.state;
|
||||||
|
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.events.GameTick;
|
||||||
|
|
||||||
|
public class TickCount implements IStateTracker
|
||||||
|
{
|
||||||
|
private int tickCount = 0;
|
||||||
|
|
||||||
|
public int get()
|
||||||
|
{
|
||||||
|
return tickCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onGameTick(Client client, GameTick tick)
|
||||||
|
{
|
||||||
|
tickCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isWithinNTicks(int toCheckAgainst, int n)
|
||||||
|
{
|
||||||
|
return this.tickCount <= toCheckAgainst + n && this.tickCount >= toCheckAgainst;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset()
|
||||||
|
{
|
||||||
|
tickCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
+177
@@ -0,0 +1,177 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat.variablespeed.state;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.helpers.combat.CombatUtils;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.NPC;
|
||||||
|
import net.runelite.api.events.ChatMessage;
|
||||||
|
import net.runelite.api.events.GameTick;
|
||||||
|
import net.runelite.api.events.NpcSpawned;
|
||||||
|
import net.runelite.api.gameval.NpcID;
|
||||||
|
|
||||||
|
public class Yama implements IStateTracker
|
||||||
|
{
|
||||||
|
private static final int HP_FUDGE = 2;
|
||||||
|
private static final int VOID_FLARE_HP_P1_P2 = 140 - HP_FUDGE;
|
||||||
|
private static final int VOID_FLARE_HP_P3 = 71 - HP_FUDGE;
|
||||||
|
private static final int YAMA_REGION_ID = 6045;
|
||||||
|
private static final int YAMA_PHASE_TRANSITION_ANIMATION_ID = 12147;
|
||||||
|
|
||||||
|
public YamaData yama;
|
||||||
|
public boolean inYamaRegion;
|
||||||
|
public NPC lastTarget;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onGameTick(Client client, GameTick tick)
|
||||||
|
{
|
||||||
|
inYamaRegion = CombatUtils.isInRegionId(client, YAMA_REGION_ID);
|
||||||
|
if (!inYamaRegion)
|
||||||
|
{
|
||||||
|
yama = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (yama != null)
|
||||||
|
{
|
||||||
|
yama.determineYamaPhase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onChatMessage(Client client, ChatMessage event)
|
||||||
|
{
|
||||||
|
if (!inYamaRegion || yama == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.getMessage().startsWith("Your Yama success count is:"))
|
||||||
|
{
|
||||||
|
yama.killed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNpcSpawned(final Client client, final NpcSpawned npcSpawned)
|
||||||
|
{
|
||||||
|
if (!inYamaRegion || yama != null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final NPC npc = npcSpawned.getNpc();
|
||||||
|
if (npc.getId() == NpcID.YAMA)
|
||||||
|
{
|
||||||
|
yama = new YamaData(npc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NPC isEitherVoidFlare(NPC a, NPC b)
|
||||||
|
{
|
||||||
|
if (a != null && a.getId() == NpcID.YAMA_VOIDFLARE)
|
||||||
|
{
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
if (b != null && b.getId() == NpcID.YAMA_VOIDFLARE)
|
||||||
|
{
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void dealVoidFlareDamage(NPC target, int damageDealt)
|
||||||
|
{
|
||||||
|
if (yama == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.yama.dealVoidFlareDamage(target, damageDealt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getVoidFlareDead(NPC target)
|
||||||
|
{
|
||||||
|
if (yama == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.yama.getVoidFlareDead(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
public YamaPhase phase()
|
||||||
|
{
|
||||||
|
if (yama == null)
|
||||||
|
{
|
||||||
|
return YamaPhase.P1;
|
||||||
|
}
|
||||||
|
return this.yama.phase;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class YamaData
|
||||||
|
{
|
||||||
|
@NonNull
|
||||||
|
private final NPC yama;
|
||||||
|
private YamaPhase phase;
|
||||||
|
private int phaseChangeCooldown;
|
||||||
|
private final Map<NPC, Integer> voidFlares;
|
||||||
|
|
||||||
|
YamaData(@NonNull NPC yama)
|
||||||
|
{
|
||||||
|
this.yama = yama;
|
||||||
|
this.phase = YamaPhase.P1;
|
||||||
|
this.phaseChangeCooldown = 0;
|
||||||
|
this.voidFlares = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getVoidFlareDead(NPC target)
|
||||||
|
{
|
||||||
|
if (!this.voidFlares.containsKey(target))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.voidFlares.get(target) <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void dealVoidFlareDamage(NPC target, int damageDealt)
|
||||||
|
{
|
||||||
|
if (this.voidFlares.containsKey(target))
|
||||||
|
{
|
||||||
|
this.voidFlares.put(target, this.voidFlares.get(target) - damageDealt);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
final int hp = this.getVoidFlareHp() - damageDealt;
|
||||||
|
this.voidFlares.put(target, hp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void determineYamaPhase()
|
||||||
|
{
|
||||||
|
if (phaseChangeCooldown == 0 && this.yama.getAnimation() == YAMA_PHASE_TRANSITION_ANIMATION_ID)
|
||||||
|
{
|
||||||
|
this.phaseChangeCooldown = 20;
|
||||||
|
this.phase = this.phase.nextPhase();
|
||||||
|
}
|
||||||
|
if (this.phaseChangeCooldown > 0)
|
||||||
|
{
|
||||||
|
this.phaseChangeCooldown--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getVoidFlareHp()
|
||||||
|
{
|
||||||
|
switch (this.phase)
|
||||||
|
{
|
||||||
|
case P1:
|
||||||
|
case P2:
|
||||||
|
return VOID_FLARE_HP_P1_P2;
|
||||||
|
default:
|
||||||
|
return VOID_FLARE_HP_P3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void killed()
|
||||||
|
{
|
||||||
|
this.phase = YamaPhase.P1;
|
||||||
|
this.voidFlares.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package com.example.essentialsplus.helpers.combat.variablespeed.state;
|
||||||
|
|
||||||
|
public enum YamaPhase
|
||||||
|
{
|
||||||
|
P1, P2, P3;
|
||||||
|
|
||||||
|
public YamaPhase nextPhase()
|
||||||
|
{
|
||||||
|
switch (this)
|
||||||
|
{
|
||||||
|
case P1:
|
||||||
|
return P2;
|
||||||
|
case P2:
|
||||||
|
return P3;
|
||||||
|
case P3:
|
||||||
|
return P3;
|
||||||
|
}
|
||||||
|
return P1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.example.essentialsplus.loot;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kills and drops of one source within a small time bucket. Recent events are kept per minute,
|
||||||
|
* older ones are merged into hourly and daily buckets (see {@link LootRecord#compactEvents}).
|
||||||
|
*/
|
||||||
|
public class LootEvent
|
||||||
|
{
|
||||||
|
/** bucket start (epoch millis) */
|
||||||
|
public long t;
|
||||||
|
/** kills in this bucket */
|
||||||
|
public int k;
|
||||||
|
/** item id -> quantity */
|
||||||
|
public Map<Integer, Long> i = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
public LootEvent()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public LootEvent(long t)
|
||||||
|
{
|
||||||
|
this.t = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
package com.example.essentialsplus.loot;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import net.runelite.client.game.ItemManager;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class LootRecord
|
||||||
|
{
|
||||||
|
private String name;
|
||||||
|
private String type; // "NPC", "CHEST", "CLUE", "EVENT", "PLAYER"
|
||||||
|
private int kills;
|
||||||
|
private long firstSeen;
|
||||||
|
private long lastSeen;
|
||||||
|
private Map<Integer, TrackedLootItem> items = new LinkedHashMap<>();
|
||||||
|
private com.example.essentialsplus.loot.combat.CombatStats combatStats;
|
||||||
|
/** Time-bucketed kills/drops, used for time range filtering. */
|
||||||
|
private List<LootEvent> events = new ArrayList<>();
|
||||||
|
|
||||||
|
private static final long MINUTE = 60_000L;
|
||||||
|
private static final long HOUR = 60 * MINUTE;
|
||||||
|
private static final long DAY = 24 * HOUR;
|
||||||
|
|
||||||
|
public boolean hasCombatStats()
|
||||||
|
{
|
||||||
|
return combatStats != null && combatStats.getFightsCount() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void addCombatFight(com.example.essentialsplus.loot.combat.FightSession session)
|
||||||
|
{
|
||||||
|
addCombatFight(session, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void addCombatFight(com.example.essentialsplus.loot.combat.FightSession session, long lootValue)
|
||||||
|
{
|
||||||
|
if (combatStats == null)
|
||||||
|
{
|
||||||
|
combatStats = new com.example.essentialsplus.loot.combat.CombatStats();
|
||||||
|
}
|
||||||
|
combatStats.addFight(session, lootValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LootRecord(String name, String type)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
this.type = type != null ? type : "NPC";
|
||||||
|
this.kills = 0;
|
||||||
|
this.firstSeen = System.currentTimeMillis();
|
||||||
|
this.lastSeen = this.firstSeen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void addDrop(int itemId, String itemName, int quantity, int gePrice)
|
||||||
|
{
|
||||||
|
this.lastSeen = System.currentTimeMillis();
|
||||||
|
currentEvent(lastSeen).i.merge(itemId, (long) quantity, Long::sum);
|
||||||
|
TrackedLootItem item = items.computeIfAbsent(itemId, id -> new TrackedLootItem(id, itemName, 0, gePrice));
|
||||||
|
item.addQuantity(quantity);
|
||||||
|
if (gePrice > 0)
|
||||||
|
{
|
||||||
|
item.setGePrice(gePrice);
|
||||||
|
}
|
||||||
|
if (itemName != null && !itemName.isEmpty())
|
||||||
|
{
|
||||||
|
item.setName(itemName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void addKills(int count)
|
||||||
|
{
|
||||||
|
this.kills += count;
|
||||||
|
this.lastSeen = System.currentTimeMillis();
|
||||||
|
currentEvent(lastSeen).k += count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized long getTotalValue()
|
||||||
|
{
|
||||||
|
long total = 0;
|
||||||
|
for (TrackedLootItem item : items.values())
|
||||||
|
{
|
||||||
|
total += item.getTotalPrice();
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized long getTotalValue(ItemManager itemManager)
|
||||||
|
{
|
||||||
|
return getTotalValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized List<TrackedLootItem> getSortedItems()
|
||||||
|
{
|
||||||
|
List<TrackedLootItem> list = new ArrayList<>(items.values());
|
||||||
|
list.sort((a, b) -> Long.compare(b.getTotalPrice(), a.getTotalPrice()));
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized List<TrackedLootItem> getSortedItems(ItemManager itemManager)
|
||||||
|
{
|
||||||
|
return getSortedItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized int getDistinctItemCount()
|
||||||
|
{
|
||||||
|
return items.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized long getTotalItemQuantity()
|
||||||
|
{
|
||||||
|
long sum = 0;
|
||||||
|
for (TrackedLootItem item : items.values())
|
||||||
|
{
|
||||||
|
sum += item.getQuantity();
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized List<LootEvent> getEvents()
|
||||||
|
{
|
||||||
|
if (events == null)
|
||||||
|
{
|
||||||
|
events = new ArrayList<>();
|
||||||
|
}
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LootEvent currentEvent(long now)
|
||||||
|
{
|
||||||
|
List<LootEvent> list = getEvents();
|
||||||
|
long bucket = now - now % MINUTE;
|
||||||
|
LootEvent last = list.isEmpty() ? null : list.get(list.size() - 1);
|
||||||
|
if (last == null || last.t != bucket)
|
||||||
|
{
|
||||||
|
last = new LootEvent(bucket);
|
||||||
|
list.add(last);
|
||||||
|
}
|
||||||
|
return last;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Merges events older than 2 days into hours and older than 60 days into days. */
|
||||||
|
public synchronized void compactEvents(long now)
|
||||||
|
{
|
||||||
|
List<LootEvent> list = getEvents();
|
||||||
|
if (list.size() < 2)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<LootEvent> out = new ArrayList<>(list.size());
|
||||||
|
for (LootEvent e : list)
|
||||||
|
{
|
||||||
|
long age = now - e.t;
|
||||||
|
long size = age > 60 * DAY ? DAY : age > 2 * DAY ? HOUR : MINUTE;
|
||||||
|
long bucket = e.t - e.t % size;
|
||||||
|
LootEvent last = out.isEmpty() ? null : out.get(out.size() - 1);
|
||||||
|
if (last != null && last.t == bucket)
|
||||||
|
{
|
||||||
|
last.k += e.k;
|
||||||
|
e.i.forEach((id, q) -> last.i.merge(id, q, Long::sum));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LootEvent copy = new LootEvent(bucket);
|
||||||
|
copy.k = e.k;
|
||||||
|
copy.i.putAll(e.i);
|
||||||
|
out.add(copy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
events = out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The part of this record in the filter's range; for "Now" only the current streak of kills/drops. */
|
||||||
|
public synchronized LootRecord view(com.example.essentialsplus.ui.TimeFilter filter)
|
||||||
|
{
|
||||||
|
if (!filter.isCurrent())
|
||||||
|
{
|
||||||
|
return since(filter.start());
|
||||||
|
}
|
||||||
|
List<LootEvent> list = getEvents();
|
||||||
|
long start = com.example.essentialsplus.ui.TimeFilter.streakStart(list.size(), i -> list.get(i).t, i -> true, System.currentTimeMillis());
|
||||||
|
return since(start == Long.MAX_VALUE ? Long.MAX_VALUE / 2 : start);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A view of this record limited to kills, drops and fights at or after {@code start}.
|
||||||
|
* Returns this record itself for {@code start <= 0} (all time).
|
||||||
|
*/
|
||||||
|
public synchronized LootRecord since(long start)
|
||||||
|
{
|
||||||
|
if (start <= 0)
|
||||||
|
{
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
LootRecord r = new LootRecord();
|
||||||
|
r.name = name;
|
||||||
|
r.type = type;
|
||||||
|
r.firstSeen = 0;
|
||||||
|
r.lastSeen = 0;
|
||||||
|
for (LootEvent e : getEvents())
|
||||||
|
{
|
||||||
|
if (e.t + MINUTE <= start)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
r.kills += e.k;
|
||||||
|
if (r.firstSeen == 0)
|
||||||
|
{
|
||||||
|
r.firstSeen = e.t;
|
||||||
|
}
|
||||||
|
r.lastSeen = Math.max(r.lastSeen, e.t);
|
||||||
|
for (Map.Entry<Integer, Long> d : e.i.entrySet())
|
||||||
|
{
|
||||||
|
TrackedLootItem base = items.get(d.getKey());
|
||||||
|
String itemName = base != null ? base.getName() : "Item " + d.getKey();
|
||||||
|
long price = base != null ? base.getGePrice() : 0;
|
||||||
|
r.items.computeIfAbsent(d.getKey(), id -> new TrackedLootItem(id, itemName, 0, price)).addQuantity(d.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (combatStats != null)
|
||||||
|
{
|
||||||
|
List<com.example.essentialsplus.loot.combat.SingleFightRecord> fights = new ArrayList<>();
|
||||||
|
for (com.example.essentialsplus.loot.combat.SingleFightRecord f : combatStats.getFightHistory())
|
||||||
|
{
|
||||||
|
if (f.getTimestamp() >= start)
|
||||||
|
{
|
||||||
|
fights.add(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!fights.isEmpty())
|
||||||
|
{
|
||||||
|
r.combatStats = com.example.essentialsplus.loot.combat.CombatStats.fromFights(fights);
|
||||||
|
r.lastSeen = Math.max(r.lastSeen, fights.get(fights.size() - 1).getTimestamp());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True if nothing was recorded (e.g. a filtered view outside the range). */
|
||||||
|
public synchronized boolean isEmpty()
|
||||||
|
{
|
||||||
|
return kills <= 0 && items.isEmpty() && !hasCombatStats();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,456 @@
|
|||||||
|
package com.example.essentialsplus.loot;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.WealthStore;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.io.Writer;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.ScheduledFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import javax.swing.SwingUtilities;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.runelite.api.ItemComposition;
|
||||||
|
import net.runelite.client.game.ItemManager;
|
||||||
|
import net.runelite.client.game.ItemStack;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Singleton
|
||||||
|
public class LootTrackerStore
|
||||||
|
{
|
||||||
|
private static final String FILE_NAME = "loot_tracker.json";
|
||||||
|
private static final Type RECORD_MAP_TYPE = new TypeToken<Map<String, LootRecord>>() {}.getType();
|
||||||
|
|
||||||
|
private final Gson gson;
|
||||||
|
private final ScheduledExecutorService executor;
|
||||||
|
private final Map<String, LootRecord> records = new ConcurrentHashMap<>();
|
||||||
|
private final List<Runnable> listeners = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
|
private final Map<Long, Boolean> recentDropHashes = Collections.synchronizedMap(new LinkedHashMap<Long, Boolean>()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
protected boolean removeEldestEntry(Map.Entry<Long, Boolean> eldest)
|
||||||
|
{
|
||||||
|
return size() > 128;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
private ScheduledFuture<?> pendingSaveTask;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public LootTrackerStore(Gson gson, ScheduledExecutorService executor)
|
||||||
|
{
|
||||||
|
this.gson = gson.newBuilder().setPrettyPrinting().disableHtmlEscaping().create();
|
||||||
|
this.executor = executor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void load()
|
||||||
|
{
|
||||||
|
File file = new File(WealthStore.DIR, FILE_NAME);
|
||||||
|
if (!file.exists())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (Reader reader = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8))
|
||||||
|
{
|
||||||
|
Map<String, LootRecord> loaded = gson.fromJson(reader, RECORD_MAP_TYPE);
|
||||||
|
if (loaded != null)
|
||||||
|
{
|
||||||
|
records.clear();
|
||||||
|
records.putAll(loaded);
|
||||||
|
|
||||||
|
// Clean up orphaned Doom minions so they do not clutter the overview
|
||||||
|
LootRecord earth = records.remove("Volatile earth");
|
||||||
|
LootRecord larva = records.remove("Demonic larva");
|
||||||
|
if (earth != null || larva != null)
|
||||||
|
{
|
||||||
|
log.info("Cleaned orphaned Doom minions (Volatile earth / Demonic larva) from records");
|
||||||
|
saveDebounced();
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
log.warn("Failed to load loot tracker records from {}", file, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void save()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Files.createDirectories(WealthStore.DIR.toPath());
|
||||||
|
File target = new File(WealthStore.DIR, FILE_NAME);
|
||||||
|
File tmp = new File(WealthStore.DIR, FILE_NAME + ".tmp");
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
for (LootRecord record : records.values())
|
||||||
|
{
|
||||||
|
record.compactEvents(now);
|
||||||
|
}
|
||||||
|
|
||||||
|
try (Writer writer = Files.newBufferedWriter(tmp.toPath(), StandardCharsets.UTF_8))
|
||||||
|
{
|
||||||
|
gson.toJson(records, RECORD_MAP_TYPE, writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
Files.move(tmp.toPath(), target.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
log.error("Failed to save loot tracker records", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void saveDebounced()
|
||||||
|
{
|
||||||
|
if (pendingSaveTask != null && !pendingSaveTask.isDone())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingSaveTask = executor.schedule(this::save, 3, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDuplicate(int tick, String source, Collection<ItemStack> items)
|
||||||
|
{
|
||||||
|
long hash = ((long) tick << 32) ^ ((long) source.hashCode() & 0xFFFFFFFFL);
|
||||||
|
if (items != null)
|
||||||
|
{
|
||||||
|
for (ItemStack item : items)
|
||||||
|
{
|
||||||
|
if (item != null)
|
||||||
|
{
|
||||||
|
hash = hash * 31 + ((long) item.getId() * 10007L + item.getQuantity());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return recentDropHashes.put(hash, Boolean.TRUE) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addLoot(String source, String type, Collection<ItemStack> items, int amount, ItemManager itemManager, int currentTick)
|
||||||
|
{
|
||||||
|
if (source == null || source.trim().isEmpty() || items == null || items.isEmpty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDuplicate(currentTick, source, items))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String cleanSource = com.example.essentialsplus.loot.combat.CombatTracker.getCanonicalEncounterName(source.trim());
|
||||||
|
LootRecord record = records.computeIfAbsent(cleanSource, s -> new LootRecord(s, type));
|
||||||
|
// amount < 0: loot without a kill (e.g. Doom reward claim; kills come from CombatTracker)
|
||||||
|
if (amount >= 0)
|
||||||
|
{
|
||||||
|
record.addKills(Math.max(1, amount));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ItemStack item : items)
|
||||||
|
{
|
||||||
|
if (item == null || item.getId() <= 0 || item.getQuantity() <= 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int id = item.getId();
|
||||||
|
int qty = item.getQuantity();
|
||||||
|
|
||||||
|
String name = "Item " + id;
|
||||||
|
int gePrice = 0;
|
||||||
|
if (itemManager != null)
|
||||||
|
{
|
||||||
|
ItemComposition comp = itemManager.getItemComposition(id);
|
||||||
|
if (comp != null && comp.getName() != null)
|
||||||
|
{
|
||||||
|
name = comp.getName();
|
||||||
|
}
|
||||||
|
gePrice = itemManager.getItemPrice(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
record.addDrop(id, name, qty, gePrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveDebounced();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteRecord(String source)
|
||||||
|
{
|
||||||
|
if (source != null && records.remove(source) != null)
|
||||||
|
{
|
||||||
|
saveDebounced();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearAll()
|
||||||
|
{
|
||||||
|
records.clear();
|
||||||
|
save();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<LootRecord> getRecords()
|
||||||
|
{
|
||||||
|
return records.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
public LootRecord getRecord(String source)
|
||||||
|
{
|
||||||
|
if (source == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String canonical = com.example.essentialsplus.loot.combat.CombatTracker.getCanonicalEncounterName(source.trim());
|
||||||
|
LootRecord r = records.get(canonical);
|
||||||
|
if (r != null)
|
||||||
|
{
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, LootRecord> entry : records.entrySet())
|
||||||
|
{
|
||||||
|
if (entry.getKey().equalsIgnoreCase(canonical))
|
||||||
|
{
|
||||||
|
return entry.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LootRecord getOrCreateRecord(String source, String type)
|
||||||
|
{
|
||||||
|
if (source == null || source.trim().isEmpty())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String clean = com.example.essentialsplus.loot.combat.CombatTracker.getCanonicalEncounterName(source.trim());
|
||||||
|
LootRecord existing = getRecord(clean);
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
LootRecord r = new LootRecord(clean, type != null ? type : "NPC");
|
||||||
|
records.put(clean, r);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Records limited to {@code start} (0 = all time); sources without activity in the range are left out. */
|
||||||
|
public List<LootRecord> getRecords(com.example.essentialsplus.ui.TimeFilter filter)
|
||||||
|
{
|
||||||
|
if (!filter.isCurrent())
|
||||||
|
{
|
||||||
|
return getRecords(filter.start());
|
||||||
|
}
|
||||||
|
List<LootRecord> out = new ArrayList<>();
|
||||||
|
for (LootRecord record : records.values())
|
||||||
|
{
|
||||||
|
LootRecord view = record.view(filter);
|
||||||
|
if (!view.isEmpty())
|
||||||
|
{
|
||||||
|
out.add(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<LootRecord> getRecords(long start)
|
||||||
|
{
|
||||||
|
List<LootRecord> out = new ArrayList<>();
|
||||||
|
for (LootRecord record : records.values())
|
||||||
|
{
|
||||||
|
LootRecord view = record.since(start);
|
||||||
|
if (start <= 0 || !view.isEmpty())
|
||||||
|
{
|
||||||
|
out.add(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Totals over a set of (possibly filtered) records. */
|
||||||
|
public static long totalValue(Collection<LootRecord> list)
|
||||||
|
{
|
||||||
|
long total = 0;
|
||||||
|
for (LootRecord record : list)
|
||||||
|
{
|
||||||
|
total += record.getTotalValue();
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int totalKills(Collection<LootRecord> list)
|
||||||
|
{
|
||||||
|
int sum = 0;
|
||||||
|
for (LootRecord record : list)
|
||||||
|
{
|
||||||
|
sum += record.getKills();
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long totalSuppliesCost(Collection<LootRecord> list)
|
||||||
|
{
|
||||||
|
long sum = 0;
|
||||||
|
for (LootRecord record : list)
|
||||||
|
{
|
||||||
|
if (record.hasCombatStats())
|
||||||
|
{
|
||||||
|
sum += record.getCombatStats().getTotalSuppliesCost();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double overallEfficiency(Collection<LootRecord> list)
|
||||||
|
{
|
||||||
|
long totalDuration = 0;
|
||||||
|
long totalIdle = 0;
|
||||||
|
for (LootRecord record : list)
|
||||||
|
{
|
||||||
|
if (record.hasCombatStats())
|
||||||
|
{
|
||||||
|
totalDuration += record.getCombatStats().getTotalDurationTicks();
|
||||||
|
totalIdle += record.getCombatStats().getTotalIdleTicks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (totalDuration <= 0)
|
||||||
|
{
|
||||||
|
return 100.0;
|
||||||
|
}
|
||||||
|
return Math.max(0.0, Math.min(100.0, (1.0 - (double) totalIdle / totalDuration) * 100.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTotalValue()
|
||||||
|
{
|
||||||
|
long total = 0;
|
||||||
|
for (LootRecord record : records.values())
|
||||||
|
{
|
||||||
|
total += record.getTotalValue();
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTotalValue(ItemManager itemManager)
|
||||||
|
{
|
||||||
|
return getTotalValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotalKills()
|
||||||
|
{
|
||||||
|
int sum = 0;
|
||||||
|
for (LootRecord record : records.values())
|
||||||
|
{
|
||||||
|
sum += record.getKills();
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTotalSuppliesCost()
|
||||||
|
{
|
||||||
|
long sum = 0;
|
||||||
|
for (LootRecord record : records.values())
|
||||||
|
{
|
||||||
|
if (record.hasCombatStats())
|
||||||
|
{
|
||||||
|
sum += record.getCombatStats().getTotalSuppliesCost();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTotalNetProfit()
|
||||||
|
{
|
||||||
|
return getTotalValue() - getTotalSuppliesCost();
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getOverallEfficiency()
|
||||||
|
{
|
||||||
|
long totalDuration = 0;
|
||||||
|
long totalIdle = 0;
|
||||||
|
for (LootRecord record : records.values())
|
||||||
|
{
|
||||||
|
if (record.hasCombatStats())
|
||||||
|
{
|
||||||
|
totalDuration += record.getCombatStats().getTotalDurationTicks();
|
||||||
|
totalIdle += record.getCombatStats().getTotalIdleTicks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (totalDuration <= 0)
|
||||||
|
{
|
||||||
|
return 100.0;
|
||||||
|
}
|
||||||
|
return Math.max(0.0, Math.min(100.0, (1.0 - (double) totalIdle / totalDuration) * 100.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<TrackedLootItem> getTopOverallItems(int limit)
|
||||||
|
{
|
||||||
|
Map<Integer, TrackedLootItem> merged = new LinkedHashMap<>();
|
||||||
|
for (LootRecord r : records.values())
|
||||||
|
{
|
||||||
|
for (TrackedLootItem item : r.getItems().values())
|
||||||
|
{
|
||||||
|
TrackedLootItem existing = merged.computeIfAbsent(item.getId(), id -> new TrackedLootItem(id, item.getName(), 0, item.getGePrice()));
|
||||||
|
existing.addQuantity(item.getQuantity());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<TrackedLootItem> list = new ArrayList<>(merged.values());
|
||||||
|
list.sort((a, b) -> Long.compare(b.getTotalPrice(), a.getTotalPrice()));
|
||||||
|
if (list.size() > limit)
|
||||||
|
{
|
||||||
|
list = new ArrayList<>(list.subList(0, limit));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<TrackedLootItem> getTopOverallItems(ItemManager itemManager, int limit)
|
||||||
|
{
|
||||||
|
return getTopOverallItems(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addChangeListener(Runnable r)
|
||||||
|
{
|
||||||
|
listeners.add(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeChangeListener(Runnable r)
|
||||||
|
{
|
||||||
|
listeners.remove(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void notifyListeners()
|
||||||
|
{
|
||||||
|
SwingUtilities.invokeLater(() ->
|
||||||
|
{
|
||||||
|
for (Runnable r : listeners)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
r.run();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
log.warn("Error running loot tracker listener", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.example.essentialsplus.loot;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import net.runelite.client.game.ItemManager;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class TrackedLootItem
|
||||||
|
{
|
||||||
|
private int id;
|
||||||
|
private String name;
|
||||||
|
private long quantity;
|
||||||
|
private long gePrice;
|
||||||
|
|
||||||
|
public void addQuantity(long qty)
|
||||||
|
{
|
||||||
|
this.quantity += qty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTotalPrice()
|
||||||
|
{
|
||||||
|
return gePrice * quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTotalPrice(ItemManager itemManager)
|
||||||
|
{
|
||||||
|
return getTotalPrice();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,342 @@
|
|||||||
|
package com.example.essentialsplus.loot.combat;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class CombatStats
|
||||||
|
{
|
||||||
|
// Cumulative totals across all kills of this enemy
|
||||||
|
private int fightsCount;
|
||||||
|
private long totalAttacksDealt;
|
||||||
|
private long totalDamageDealt;
|
||||||
|
private long totalHitsReceived;
|
||||||
|
private long totalDamageTaken;
|
||||||
|
private long totalDurationTicks;
|
||||||
|
private long totalIdleTicks;
|
||||||
|
private long totalTilesMoved;
|
||||||
|
private long totalHpHealed;
|
||||||
|
private long totalSuppliesCost;
|
||||||
|
private Map<String, Integer> totalFoodUsed = new LinkedHashMap<>();
|
||||||
|
private Map<String, Integer> totalPotionsUsed = new LinkedHashMap<>();
|
||||||
|
private Map<String, Integer> totalAmmoUsed = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
// Cumulative Prayer & Weapon Metrics
|
||||||
|
private long totalPrayerBlockedHits;
|
||||||
|
private long totalPrayerMissedHits;
|
||||||
|
private long totalOffPrayerDamageTaken;
|
||||||
|
private long totalOnPrayerAttacks;
|
||||||
|
private long totalOffPrayerAttacks;
|
||||||
|
private long totalCorrectWeaponAttacks;
|
||||||
|
private long totalIncorrectWeaponAttacks;
|
||||||
|
|
||||||
|
// Delve metrics
|
||||||
|
private int maxDelve;
|
||||||
|
private int lastDelve;
|
||||||
|
|
||||||
|
// Most recent fight snapshot
|
||||||
|
private int lastAttacksDealt;
|
||||||
|
private int lastDamageDealt;
|
||||||
|
private int lastHitsReceived;
|
||||||
|
private int lastDamageTaken;
|
||||||
|
private int lastDurationTicks;
|
||||||
|
private int lastIdleTicks;
|
||||||
|
private int lastTilesMoved;
|
||||||
|
private int lastHpHealed;
|
||||||
|
private long lastSuppliesCost;
|
||||||
|
private double lastEfficiency;
|
||||||
|
private double lastPerformanceScore;
|
||||||
|
private Map<String, Integer> lastFoodUsed = new LinkedHashMap<>();
|
||||||
|
private Map<String, Integer> lastPotionsUsed = new LinkedHashMap<>();
|
||||||
|
private Map<String, Integer> lastAmmoUsed = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
private int lastPrayerBlockedHits;
|
||||||
|
private int lastPrayerMissedHits;
|
||||||
|
private int lastOffPrayerDamageTaken;
|
||||||
|
private int lastOnPrayerAttacks;
|
||||||
|
private int lastOffPrayerAttacks;
|
||||||
|
private int lastCorrectWeaponAttacks;
|
||||||
|
private int lastIncorrectWeaponAttacks;
|
||||||
|
|
||||||
|
/** Individual fights kept per enemy; also bounds how far back time range filters see fights. */
|
||||||
|
private static final int MAX_HISTORY = 1000;
|
||||||
|
|
||||||
|
// List of all individual fights recorded
|
||||||
|
private List<SingleFightRecord> fightHistory = new ArrayList<>();
|
||||||
|
|
||||||
|
public synchronized void addFight(FightSession session)
|
||||||
|
{
|
||||||
|
addFight(session, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void addFight(FightSession session, long lootValue)
|
||||||
|
{
|
||||||
|
this.fightsCount++;
|
||||||
|
|
||||||
|
this.totalAttacksDealt += session.getAttacksDealt();
|
||||||
|
this.totalDamageDealt += session.getDamageDealt();
|
||||||
|
this.totalHitsReceived += session.getHitsReceived();
|
||||||
|
this.totalDamageTaken += session.getDamageTaken();
|
||||||
|
this.totalDurationTicks += session.getDurationTicks();
|
||||||
|
this.totalIdleTicks += session.getIdleTicks();
|
||||||
|
this.totalTilesMoved += session.getTilesMoved();
|
||||||
|
this.totalHpHealed += session.getHpHealed();
|
||||||
|
this.totalSuppliesCost += session.getSuppliesCost();
|
||||||
|
|
||||||
|
session.getFoodUsed().forEach((k, v) -> this.totalFoodUsed.merge(k, v, Integer::sum));
|
||||||
|
session.getPotionsUsed().forEach((k, v) -> this.totalPotionsUsed.merge(k, v, Integer::sum));
|
||||||
|
session.getAmmoUsed().forEach((k, v) -> this.totalAmmoUsed.merge(k, v, Integer::sum));
|
||||||
|
|
||||||
|
// Prayer & Weapon totals
|
||||||
|
this.totalPrayerBlockedHits += session.getPrayerBlockedHits();
|
||||||
|
this.totalPrayerMissedHits += session.getPrayerMissedHits();
|
||||||
|
this.totalOffPrayerDamageTaken += session.getOffPrayerDamageTaken();
|
||||||
|
this.totalOnPrayerAttacks += session.getOnPrayerAttacks();
|
||||||
|
this.totalOffPrayerAttacks += session.getOffPrayerAttacks();
|
||||||
|
this.totalCorrectWeaponAttacks += session.getCorrectWeaponAttacks();
|
||||||
|
this.totalIncorrectWeaponAttacks += session.getIncorrectWeaponAttacks();
|
||||||
|
|
||||||
|
// Store last fight
|
||||||
|
this.lastAttacksDealt = session.getAttacksDealt();
|
||||||
|
this.lastDamageDealt = session.getDamageDealt();
|
||||||
|
this.lastHitsReceived = session.getHitsReceived();
|
||||||
|
this.lastDamageTaken = session.getDamageTaken();
|
||||||
|
this.lastDurationTicks = session.getDurationTicks();
|
||||||
|
this.lastIdleTicks = session.getIdleTicks();
|
||||||
|
this.lastTilesMoved = session.getTilesMoved();
|
||||||
|
this.lastHpHealed = session.getHpHealed();
|
||||||
|
this.lastSuppliesCost = session.getSuppliesCost();
|
||||||
|
this.lastEfficiency = session.getEfficiencyPct();
|
||||||
|
this.lastPerformanceScore = session.getPerformanceScore();
|
||||||
|
this.lastFoodUsed = new LinkedHashMap<>(session.getFoodUsed());
|
||||||
|
this.lastPotionsUsed = new LinkedHashMap<>(session.getPotionsUsed());
|
||||||
|
this.lastAmmoUsed = new LinkedHashMap<>(session.getAmmoUsed());
|
||||||
|
|
||||||
|
this.lastPrayerBlockedHits = session.getPrayerBlockedHits();
|
||||||
|
this.lastPrayerMissedHits = session.getPrayerMissedHits();
|
||||||
|
this.lastOffPrayerDamageTaken = session.getOffPrayerDamageTaken();
|
||||||
|
this.lastOnPrayerAttacks = session.getOnPrayerAttacks();
|
||||||
|
this.lastOffPrayerAttacks = session.getOffPrayerAttacks();
|
||||||
|
this.lastCorrectWeaponAttacks = session.getCorrectWeaponAttacks();
|
||||||
|
this.lastIncorrectWeaponAttacks = session.getIncorrectWeaponAttacks();
|
||||||
|
|
||||||
|
int delve = session.getDelve();
|
||||||
|
if (delve > this.maxDelve)
|
||||||
|
{
|
||||||
|
this.maxDelve = delve;
|
||||||
|
}
|
||||||
|
this.lastDelve = delve;
|
||||||
|
|
||||||
|
// Create SingleFightRecord for history
|
||||||
|
SingleFightRecord single = new SingleFightRecord(
|
||||||
|
this.fightsCount,
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
session.getDurationTicks(),
|
||||||
|
session.getIdleTicks(),
|
||||||
|
session.getEfficiencyPct(),
|
||||||
|
session.getGrade(),
|
||||||
|
session.getAttacksDealt(),
|
||||||
|
session.getDamageDealt(),
|
||||||
|
session.getHitsReceived(),
|
||||||
|
session.getDamageTaken(),
|
||||||
|
session.getHpHealed(),
|
||||||
|
session.getTilesMoved(),
|
||||||
|
session.getSuppliesCost(),
|
||||||
|
lootValue,
|
||||||
|
lootValue - session.getSuppliesCost(),
|
||||||
|
new LinkedHashMap<>(session.getFoodUsed()),
|
||||||
|
new LinkedHashMap<>(session.getPotionsUsed()),
|
||||||
|
new LinkedHashMap<>(session.getAmmoUsed()),
|
||||||
|
session.getPrayerBlockedHits(),
|
||||||
|
session.getPrayerMissedHits(),
|
||||||
|
session.getOffPrayerDamageTaken(),
|
||||||
|
session.getOnPrayerAttacks(),
|
||||||
|
session.getOffPrayerAttacks(),
|
||||||
|
session.getCorrectWeaponAttacks(),
|
||||||
|
session.getIncorrectWeaponAttacks(),
|
||||||
|
session.getPerformanceScore(),
|
||||||
|
delve
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.fightHistory == null)
|
||||||
|
{
|
||||||
|
this.fightHistory = new ArrayList<>();
|
||||||
|
}
|
||||||
|
this.fightHistory.add(single);
|
||||||
|
if (this.fightHistory.size() > MAX_HISTORY)
|
||||||
|
{
|
||||||
|
this.fightHistory.remove(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Rebuilds cumulative stats from a subset of fights (used for time range filtering). */
|
||||||
|
public static CombatStats fromFights(List<SingleFightRecord> fights)
|
||||||
|
{
|
||||||
|
CombatStats cs = new CombatStats();
|
||||||
|
for (SingleFightRecord f : fights)
|
||||||
|
{
|
||||||
|
cs.fightsCount++;
|
||||||
|
cs.totalAttacksDealt += f.getAttacksDealt();
|
||||||
|
cs.totalDamageDealt += f.getDamageDealt();
|
||||||
|
cs.totalHitsReceived += f.getHitsReceived();
|
||||||
|
cs.totalDamageTaken += f.getDamageTaken();
|
||||||
|
cs.totalDurationTicks += f.getDurationTicks();
|
||||||
|
cs.totalIdleTicks += f.getIdleTicks();
|
||||||
|
cs.totalTilesMoved += f.getTilesMoved();
|
||||||
|
cs.totalHpHealed += f.getHpHealed();
|
||||||
|
cs.totalSuppliesCost += f.getSuppliesCost();
|
||||||
|
if (f.getFoodUsed() != null)
|
||||||
|
{
|
||||||
|
f.getFoodUsed().forEach((k, v) -> cs.totalFoodUsed.merge(k, v, Integer::sum));
|
||||||
|
}
|
||||||
|
if (f.getPotionsUsed() != null)
|
||||||
|
{
|
||||||
|
f.getPotionsUsed().forEach((k, v) -> cs.totalPotionsUsed.merge(k, v, Integer::sum));
|
||||||
|
}
|
||||||
|
if (f.getAmmoUsed() != null)
|
||||||
|
{
|
||||||
|
f.getAmmoUsed().forEach((k, v) -> cs.totalAmmoUsed.merge(k, v, Integer::sum));
|
||||||
|
}
|
||||||
|
cs.totalPrayerBlockedHits += f.getPrayerBlockedHits();
|
||||||
|
cs.totalPrayerMissedHits += f.getPrayerMissedHits();
|
||||||
|
cs.totalOffPrayerDamageTaken += f.getOffPrayerDamageTaken();
|
||||||
|
cs.totalOnPrayerAttacks += f.getOnPrayerAttacks();
|
||||||
|
cs.totalOffPrayerAttacks += f.getOffPrayerAttacks();
|
||||||
|
cs.totalCorrectWeaponAttacks += f.getCorrectWeaponAttacks();
|
||||||
|
cs.totalIncorrectWeaponAttacks += f.getIncorrectWeaponAttacks();
|
||||||
|
cs.maxDelve = Math.max(cs.maxDelve, f.getDelve());
|
||||||
|
|
||||||
|
cs.lastAttacksDealt = f.getAttacksDealt();
|
||||||
|
cs.lastDamageDealt = f.getDamageDealt();
|
||||||
|
cs.lastHitsReceived = f.getHitsReceived();
|
||||||
|
cs.lastDamageTaken = f.getDamageTaken();
|
||||||
|
cs.lastDurationTicks = f.getDurationTicks();
|
||||||
|
cs.lastIdleTicks = f.getIdleTicks();
|
||||||
|
cs.lastTilesMoved = f.getTilesMoved();
|
||||||
|
cs.lastHpHealed = f.getHpHealed();
|
||||||
|
cs.lastSuppliesCost = f.getSuppliesCost();
|
||||||
|
cs.lastEfficiency = f.getEfficiency();
|
||||||
|
cs.lastPerformanceScore = f.getPerformanceScore();
|
||||||
|
cs.lastFoodUsed = f.getFoodUsed() == null ? new LinkedHashMap<>() : new LinkedHashMap<>(f.getFoodUsed());
|
||||||
|
cs.lastPotionsUsed = f.getPotionsUsed() == null ? new LinkedHashMap<>() : new LinkedHashMap<>(f.getPotionsUsed());
|
||||||
|
cs.lastAmmoUsed = f.getAmmoUsed() == null ? new LinkedHashMap<>() : new LinkedHashMap<>(f.getAmmoUsed());
|
||||||
|
cs.lastPrayerBlockedHits = f.getPrayerBlockedHits();
|
||||||
|
cs.lastPrayerMissedHits = f.getPrayerMissedHits();
|
||||||
|
cs.lastOffPrayerDamageTaken = f.getOffPrayerDamageTaken();
|
||||||
|
cs.lastOnPrayerAttacks = f.getOnPrayerAttacks();
|
||||||
|
cs.lastOffPrayerAttacks = f.getOffPrayerAttacks();
|
||||||
|
cs.lastCorrectWeaponAttacks = f.getCorrectWeaponAttacks();
|
||||||
|
cs.lastIncorrectWeaponAttacks = f.getIncorrectWeaponAttacks();
|
||||||
|
cs.lastDelve = f.getDelve();
|
||||||
|
}
|
||||||
|
cs.fightHistory = new ArrayList<>(fights);
|
||||||
|
return cs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SingleFightRecord> getFightHistory()
|
||||||
|
{
|
||||||
|
if (fightHistory == null)
|
||||||
|
{
|
||||||
|
fightHistory = new ArrayList<>();
|
||||||
|
}
|
||||||
|
return fightHistory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getAvgEfficiency()
|
||||||
|
{
|
||||||
|
if (totalDurationTicks <= 0)
|
||||||
|
{
|
||||||
|
return 100.0;
|
||||||
|
}
|
||||||
|
double idleRatio = (double) totalIdleTicks / totalDurationTicks;
|
||||||
|
return Math.max(0.0, Math.min(100.0, (1.0 - idleRatio) * 100.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getAvgPrayerDefensePct()
|
||||||
|
{
|
||||||
|
long total = totalPrayerBlockedHits + totalPrayerMissedHits;
|
||||||
|
if (total <= 0)
|
||||||
|
{
|
||||||
|
return 100.0;
|
||||||
|
}
|
||||||
|
return ((double) totalPrayerBlockedHits / total) * 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getAvgOffPrayerAttackPct()
|
||||||
|
{
|
||||||
|
long total = totalOnPrayerAttacks + totalOffPrayerAttacks;
|
||||||
|
if (total <= 0)
|
||||||
|
{
|
||||||
|
return 100.0;
|
||||||
|
}
|
||||||
|
return ((double) totalOffPrayerAttacks / total) * 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getAvgCorrectWeaponPct()
|
||||||
|
{
|
||||||
|
long total = totalCorrectWeaponAttacks + totalIncorrectWeaponAttacks;
|
||||||
|
if (total <= 0)
|
||||||
|
{
|
||||||
|
return 100.0;
|
||||||
|
}
|
||||||
|
return ((double) totalCorrectWeaponAttacks / total) * 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getAvgPerformanceScore()
|
||||||
|
{
|
||||||
|
double uptime = getAvgEfficiency();
|
||||||
|
long totalIncoming = totalPrayerBlockedHits + totalPrayerMissedHits;
|
||||||
|
boolean defenseTested = (totalIncoming > 0);
|
||||||
|
double defScore = getAvgPrayerDefensePct();
|
||||||
|
|
||||||
|
long totalOverheadAttacks = totalOnPrayerAttacks + totalOffPrayerAttacks;
|
||||||
|
boolean overheadTested = (totalOverheadAttacks > 0 && totalOnPrayerAttacks > 0);
|
||||||
|
double offScore = getAvgOffPrayerAttackPct();
|
||||||
|
|
||||||
|
long totalWep = totalCorrectWeaponAttacks + totalIncorrectWeaponAttacks;
|
||||||
|
boolean wepTested = (totalWep > 0 && totalIncorrectWeaponAttacks > 0);
|
||||||
|
double wepScore = getAvgCorrectWeaponPct();
|
||||||
|
|
||||||
|
if (defenseTested && (overheadTested || wepTested))
|
||||||
|
{
|
||||||
|
double offense = (offScore * 0.5) + (wepScore * 0.5);
|
||||||
|
return Math.max(0.0, Math.min(100.0, (0.40 * uptime) + (0.35 * defScore) + (0.25 * offense)));
|
||||||
|
}
|
||||||
|
else if (defenseTested)
|
||||||
|
{
|
||||||
|
return Math.max(0.0, Math.min(100.0, (0.60 * uptime) + (0.40 * defScore)));
|
||||||
|
}
|
||||||
|
else if (overheadTested || wepTested)
|
||||||
|
{
|
||||||
|
double offense = (offScore * 0.5) + (wepScore * 0.5);
|
||||||
|
return Math.max(0.0, Math.min(100.0, (0.60 * uptime) + (0.40 * offense)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return uptime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLastGrade()
|
||||||
|
{
|
||||||
|
return getGrade(lastPerformanceScore > 0 ? lastPerformanceScore : lastEfficiency);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAvgGrade()
|
||||||
|
{
|
||||||
|
return getGrade(getAvgPerformanceScore());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getGrade(double score)
|
||||||
|
{
|
||||||
|
if (score >= 95.0) return "S+";
|
||||||
|
if (score >= 90.0) return "A+";
|
||||||
|
if (score >= 80.0) return "A";
|
||||||
|
if (score >= 70.0) return "B";
|
||||||
|
if (score >= 55.0) return "C";
|
||||||
|
return "D";
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,279 @@
|
|||||||
|
package com.example.essentialsplus.loot.combat;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import net.runelite.api.coords.WorldPoint;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class FightSession
|
||||||
|
{
|
||||||
|
private final String targetName;
|
||||||
|
@Setter
|
||||||
|
private int targetIndex;
|
||||||
|
private final int startTick;
|
||||||
|
private int targetCombatLevel;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
private int lastCombatTick;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
private int delve = 0;
|
||||||
|
|
||||||
|
private int attacksDealt;
|
||||||
|
private int damageDealt;
|
||||||
|
private int hitsReceived;
|
||||||
|
private int damageTaken;
|
||||||
|
private int durationTicks;
|
||||||
|
private int idleTicks;
|
||||||
|
private int tilesMoved;
|
||||||
|
private int hpHealed;
|
||||||
|
private long suppliesCost;
|
||||||
|
|
||||||
|
private int weaponSpeed = 4;
|
||||||
|
private int ticksSinceLastAttack = 0;
|
||||||
|
private int lastAttackTick = -1;
|
||||||
|
private WorldPoint lastLocation;
|
||||||
|
|
||||||
|
private final Map<String, Integer> foodUsed = new LinkedHashMap<>();
|
||||||
|
private final Map<String, Integer> potionsUsed = new LinkedHashMap<>();
|
||||||
|
private final Map<String, Integer> ammoUsed = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
// Prayer & Weapon Performance metrics
|
||||||
|
private int prayerBlockedHits;
|
||||||
|
private int prayerMissedHits;
|
||||||
|
private int offPrayerDamageTaken;
|
||||||
|
private int onPrayerAttacks;
|
||||||
|
private int offPrayerAttacks;
|
||||||
|
private int correctWeaponAttacks;
|
||||||
|
private int incorrectWeaponAttacks;
|
||||||
|
|
||||||
|
public FightSession(String targetName, int targetIndex, int startTick, WorldPoint initialLocation, int initialWeaponSpeed)
|
||||||
|
{
|
||||||
|
this(targetName, targetIndex, startTick, initialLocation, initialWeaponSpeed, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FightSession(String targetName, int targetIndex, int startTick, WorldPoint initialLocation, int initialWeaponSpeed, int targetCombatLevel)
|
||||||
|
{
|
||||||
|
this.targetName = targetName;
|
||||||
|
this.targetIndex = targetIndex;
|
||||||
|
this.startTick = startTick;
|
||||||
|
this.lastCombatTick = startTick;
|
||||||
|
this.lastLocation = initialLocation;
|
||||||
|
this.weaponSpeed = Math.max(2, initialWeaponSpeed);
|
||||||
|
this.ticksSinceLastAttack = 0;
|
||||||
|
this.targetCombatLevel = targetCombatLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recordAttack(int attackSpeed, int currentTick)
|
||||||
|
{
|
||||||
|
if (currentTick == lastAttackTick && currentTick > 0)
|
||||||
|
{
|
||||||
|
return; // Avoid duplicate attack recording on the exact same tick
|
||||||
|
}
|
||||||
|
this.lastAttackTick = currentTick;
|
||||||
|
this.attacksDealt++;
|
||||||
|
this.weaponSpeed = Math.max(2, attackSpeed);
|
||||||
|
this.ticksSinceLastAttack = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recordAttackAgainstEnemy(boolean isOnPrayer, boolean isCorrectWeapon)
|
||||||
|
{
|
||||||
|
if (isOnPrayer)
|
||||||
|
{
|
||||||
|
this.onPrayerAttacks++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.offPrayerAttacks++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCorrectWeapon)
|
||||||
|
{
|
||||||
|
this.correctWeaponAttacks++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.incorrectWeaponAttacks++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recordDamageDealt(int amount)
|
||||||
|
{
|
||||||
|
if (amount > 0)
|
||||||
|
{
|
||||||
|
this.damageDealt += amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recordHitReceived(int amount)
|
||||||
|
{
|
||||||
|
this.hitsReceived++;
|
||||||
|
if (amount > 0)
|
||||||
|
{
|
||||||
|
this.damageTaken += amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recordPrayerBlock()
|
||||||
|
{
|
||||||
|
this.prayerBlockedHits++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recordPrayerMiss(int damageAmount)
|
||||||
|
{
|
||||||
|
this.prayerMissedHits++;
|
||||||
|
if (damageAmount > 0)
|
||||||
|
{
|
||||||
|
this.offPrayerDamageTaken += damageAmount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetTicksSinceLastAttack()
|
||||||
|
{
|
||||||
|
this.ticksSinceLastAttack = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recordFood(String foodName, int heal, long cost)
|
||||||
|
{
|
||||||
|
recordFood(foodName, 1, heal, cost);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recordFood(String foodName, int quantity, int heal, long cost)
|
||||||
|
{
|
||||||
|
int q = Math.max(1, quantity);
|
||||||
|
this.foodUsed.merge(foodName, q, Integer::sum);
|
||||||
|
this.hpHealed += heal;
|
||||||
|
this.suppliesCost += cost;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recordPotion(String potionName, long costPerDose)
|
||||||
|
{
|
||||||
|
recordPotion(potionName, 1, costPerDose);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recordPotion(String potionName, int quantity, long costPerDose)
|
||||||
|
{
|
||||||
|
int q = Math.max(1, quantity);
|
||||||
|
this.potionsUsed.merge(potionName, q, Integer::sum);
|
||||||
|
this.suppliesCost += costPerDose;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recordAmmo(String ammoName, int count, long totalCost)
|
||||||
|
{
|
||||||
|
this.ammoUsed.merge(ammoName, count, Integer::sum);
|
||||||
|
this.suppliesCost += totalCost;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addHpHealed(int amount)
|
||||||
|
{
|
||||||
|
this.hpHealed += amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onGameTick(WorldPoint currentLocation)
|
||||||
|
{
|
||||||
|
this.durationTicks++;
|
||||||
|
|
||||||
|
if (lastLocation != null && currentLocation != null)
|
||||||
|
{
|
||||||
|
int dist = currentLocation.distanceTo(lastLocation);
|
||||||
|
if (dist > 0 && dist < 15)
|
||||||
|
{
|
||||||
|
this.tilesMoved += dist;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.lastLocation = currentLocation;
|
||||||
|
|
||||||
|
// Tick elapsed
|
||||||
|
this.ticksSinceLastAttack++;
|
||||||
|
|
||||||
|
// If weapon takes 4 ticks, off cooldown starting at tick 5:
|
||||||
|
// When ticksSinceLastAttack > weaponSpeed (e.g. > 4), player was off cooldown and did not attack -> idle tick!
|
||||||
|
if (this.ticksSinceLastAttack > this.weaponSpeed)
|
||||||
|
{
|
||||||
|
this.idleTicks++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getEfficiencyPct()
|
||||||
|
{
|
||||||
|
if (durationTicks <= 0)
|
||||||
|
{
|
||||||
|
return 100.0;
|
||||||
|
}
|
||||||
|
double idleRatio = (double) idleTicks / durationTicks;
|
||||||
|
return Math.max(0.0, Math.min(100.0, (1.0 - idleRatio) * 100.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getPrayerDefensePct()
|
||||||
|
{
|
||||||
|
int total = prayerBlockedHits + prayerMissedHits;
|
||||||
|
if (total <= 0)
|
||||||
|
{
|
||||||
|
return 100.0;
|
||||||
|
}
|
||||||
|
return ((double) prayerBlockedHits / total) * 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getOffPrayerAttackPct()
|
||||||
|
{
|
||||||
|
int total = onPrayerAttacks + offPrayerAttacks;
|
||||||
|
if (total <= 0)
|
||||||
|
{
|
||||||
|
return 100.0;
|
||||||
|
}
|
||||||
|
return ((double) offPrayerAttacks / total) * 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getCorrectWeaponPct()
|
||||||
|
{
|
||||||
|
int total = correctWeaponAttacks + incorrectWeaponAttacks;
|
||||||
|
if (total <= 0)
|
||||||
|
{
|
||||||
|
return 100.0;
|
||||||
|
}
|
||||||
|
return ((double) correctWeaponAttacks / total) * 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates composite performance score taking uptime, defensive prayer, and offensive weapon/prayer into account.
|
||||||
|
*/
|
||||||
|
public double getPerformanceScore()
|
||||||
|
{
|
||||||
|
double uptime = getEfficiencyPct();
|
||||||
|
int totalIncoming = prayerBlockedHits + prayerMissedHits;
|
||||||
|
boolean defenseTested = (totalIncoming > 0);
|
||||||
|
double defScore = getPrayerDefensePct();
|
||||||
|
|
||||||
|
int totalOverheadAttacks = onPrayerAttacks + offPrayerAttacks;
|
||||||
|
boolean overheadTested = (totalOverheadAttacks > 0 && onPrayerAttacks > 0);
|
||||||
|
double offScore = getOffPrayerAttackPct();
|
||||||
|
|
||||||
|
int totalWep = correctWeaponAttacks + incorrectWeaponAttacks;
|
||||||
|
boolean wepTested = (totalWep > 0 && incorrectWeaponAttacks > 0);
|
||||||
|
double wepScore = getCorrectWeaponPct();
|
||||||
|
|
||||||
|
if (defenseTested && (overheadTested || wepTested))
|
||||||
|
{
|
||||||
|
double offense = (offScore * 0.5) + (wepScore * 0.5);
|
||||||
|
return Math.max(0.0, Math.min(100.0, (0.40 * uptime) + (0.35 * defScore) + (0.25 * offense)));
|
||||||
|
}
|
||||||
|
else if (defenseTested)
|
||||||
|
{
|
||||||
|
return Math.max(0.0, Math.min(100.0, (0.60 * uptime) + (0.40 * defScore)));
|
||||||
|
}
|
||||||
|
else if (overheadTested || wepTested)
|
||||||
|
{
|
||||||
|
double offense = (offScore * 0.5) + (wepScore * 0.5);
|
||||||
|
return Math.max(0.0, Math.min(100.0, (0.60 * uptime) + (0.40 * offense)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return uptime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getGrade()
|
||||||
|
{
|
||||||
|
return CombatStats.getGrade(getPerformanceScore());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.example.essentialsplus.loot.combat;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class SingleFightRecord
|
||||||
|
{
|
||||||
|
private int fightNumber;
|
||||||
|
private long timestamp;
|
||||||
|
private int durationTicks;
|
||||||
|
private int idleTicks;
|
||||||
|
private double efficiency;
|
||||||
|
private String grade;
|
||||||
|
private int attacksDealt;
|
||||||
|
private int damageDealt;
|
||||||
|
private int hitsReceived;
|
||||||
|
private int damageTaken;
|
||||||
|
private int hpHealed;
|
||||||
|
private int tilesMoved;
|
||||||
|
private long suppliesCost;
|
||||||
|
private long lootValue;
|
||||||
|
private long netProfit;
|
||||||
|
private Map<String, Integer> foodUsed = new LinkedHashMap<>();
|
||||||
|
private Map<String, Integer> potionsUsed = new LinkedHashMap<>();
|
||||||
|
private Map<String, Integer> ammoUsed = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
// Prayer & Weapon Performance metrics
|
||||||
|
private int prayerBlockedHits;
|
||||||
|
private int prayerMissedHits;
|
||||||
|
private int offPrayerDamageTaken;
|
||||||
|
private int onPrayerAttacks;
|
||||||
|
private int offPrayerAttacks;
|
||||||
|
private int correctWeaponAttacks;
|
||||||
|
private int incorrectWeaponAttacks;
|
||||||
|
private double performanceScore;
|
||||||
|
|
||||||
|
// Delve level (if Doom of Mokhaiotl or delve encounter)
|
||||||
|
private int delve;
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package com.example.essentialsplus.loot.droptable;
|
||||||
|
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/** One row of a wiki drop table ("dropsline" bucket). */
|
||||||
|
public class DropLine
|
||||||
|
{
|
||||||
|
private static final Pattern FRACTION = Pattern.compile("([\\d.]+)\\s*/\\s*([\\d.]+)");
|
||||||
|
|
||||||
|
public final String source;
|
||||||
|
public final String item;
|
||||||
|
public final String rarityText;
|
||||||
|
/** chance per roll, NaN when the wiki gives no number (e.g. "Varies") */
|
||||||
|
public final double chance;
|
||||||
|
public final int rolls;
|
||||||
|
public final int qtyLow;
|
||||||
|
public final int qtyHigh;
|
||||||
|
|
||||||
|
public DropLine(String source, String item, String rarityText, int rolls, int qtyLow, int qtyHigh)
|
||||||
|
{
|
||||||
|
this.source = source == null ? "" : source;
|
||||||
|
this.item = item == null ? "" : item;
|
||||||
|
this.rarityText = rarityText == null ? "" : rarityText;
|
||||||
|
this.chance = parseRarity(this.rarityText);
|
||||||
|
this.rolls = Math.max(1, rolls);
|
||||||
|
this.qtyLow = Math.max(0, qtyLow);
|
||||||
|
this.qtyHigh = Math.max(this.qtyLow, qtyHigh);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Expected number of times this line is hit per kill. */
|
||||||
|
public double hitsPerKill()
|
||||||
|
{
|
||||||
|
return Double.isNaN(chance) ? Double.NaN : Math.min(chance * rolls, rolls);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "Always" -> 1, "3/128" or "~1/1,024" -> fraction, anything else -> NaN. */
|
||||||
|
static double parseRarity(String text)
|
||||||
|
{
|
||||||
|
String t = text.replace(",", "").trim();
|
||||||
|
if (t.equalsIgnoreCase("always"))
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
Matcher m = FRACTION.matcher(t);
|
||||||
|
if (!m.find())
|
||||||
|
{
|
||||||
|
return Double.NaN;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
double num = Double.parseDouble(m.group(1));
|
||||||
|
double den = Double.parseDouble(m.group(2));
|
||||||
|
return den > 0 ? Math.min(1, num / den) : Double.NaN;
|
||||||
|
}
|
||||||
|
catch (NumberFormatException e)
|
||||||
|
{
|
||||||
|
return Double.NaN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
package com.example.essentialsplus.loot.droptable;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/** The wiki drop table of one page, split into its variants (e.g. "Standard", "Wilderness Slayer Cave"). */
|
||||||
|
public class DropTable
|
||||||
|
{
|
||||||
|
public final String page;
|
||||||
|
public final List<Variant> variants;
|
||||||
|
|
||||||
|
public static final class Variant
|
||||||
|
{
|
||||||
|
public final String name;
|
||||||
|
public final List<DropLine> lines;
|
||||||
|
|
||||||
|
Variant(String name, List<DropLine> lines)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
this.lines = lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Combined wiki rate of one item. */
|
||||||
|
public static final class Rate
|
||||||
|
{
|
||||||
|
public final String item;
|
||||||
|
public final String text;
|
||||||
|
/** expected hits per kill (sum over all lines of the item), NaN if unknown */
|
||||||
|
public final double perKill;
|
||||||
|
|
||||||
|
Rate(String item, String text, double perKill)
|
||||||
|
{
|
||||||
|
this.item = item;
|
||||||
|
this.text = text;
|
||||||
|
this.perKill = perKill;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DropTable(String page, List<DropLine> lines)
|
||||||
|
{
|
||||||
|
this.page = page;
|
||||||
|
this.variants = buildVariants(page, lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEmpty()
|
||||||
|
{
|
||||||
|
return variants.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groups lines by their "Dropped from" section. Small sections (a few extra drops such as Catacombs
|
||||||
|
* shards) are added on top of the main table; large ones are separate tables.
|
||||||
|
*/
|
||||||
|
static List<Variant> buildVariants(String page, List<DropLine> lines)
|
||||||
|
{
|
||||||
|
Map<String, List<DropLine>> groups = new LinkedHashMap<>();
|
||||||
|
for (DropLine l : lines)
|
||||||
|
{
|
||||||
|
groups.computeIfAbsent(l.source, k -> new ArrayList<>()).add(l);
|
||||||
|
}
|
||||||
|
List<Variant> out = new ArrayList<>();
|
||||||
|
if (groups.isEmpty())
|
||||||
|
{
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
int max = groups.values().stream().mapToInt(List::size).max().orElse(0);
|
||||||
|
List<String> mains = new ArrayList<>();
|
||||||
|
List<String> extras = new ArrayList<>();
|
||||||
|
for (Map.Entry<String, List<DropLine>> g : groups.entrySet())
|
||||||
|
{
|
||||||
|
boolean base = !g.getKey().contains("#");
|
||||||
|
(base || g.getValue().size() * 3 >= max ? mains : extras).add(g.getKey());
|
||||||
|
}
|
||||||
|
// a base section without '#' always comes first
|
||||||
|
mains.sort(Comparator.comparing(s -> s.contains("#")));
|
||||||
|
if (mains.isEmpty())
|
||||||
|
{
|
||||||
|
mains.add(extras.remove(0));
|
||||||
|
}
|
||||||
|
for (String m : mains)
|
||||||
|
{
|
||||||
|
out.add(new Variant(label(page, m), groups.get(m)));
|
||||||
|
}
|
||||||
|
String first = mains.get(0);
|
||||||
|
for (String e : extras)
|
||||||
|
{
|
||||||
|
List<DropLine> combined = new ArrayList<>(groups.get(first));
|
||||||
|
combined.addAll(groups.get(e));
|
||||||
|
out.add(new Variant(label(page, first) + " + " + label(page, e), combined));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String label(String page, String source)
|
||||||
|
{
|
||||||
|
int hash = source.indexOf('#');
|
||||||
|
if (hash >= 0 && hash + 1 < source.length())
|
||||||
|
{
|
||||||
|
return source.substring(hash + 1);
|
||||||
|
}
|
||||||
|
return source.isEmpty() ? page : "Standard";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Drop rate per item of the variant, keyed by lower-case item name. */
|
||||||
|
public static Map<String, Rate> rates(Variant variant)
|
||||||
|
{
|
||||||
|
Map<String, List<DropLine>> byItem = new LinkedHashMap<>();
|
||||||
|
for (DropLine l : variant.lines)
|
||||||
|
{
|
||||||
|
if (!l.item.isEmpty() && !l.item.equalsIgnoreCase("Nothing"))
|
||||||
|
{
|
||||||
|
byItem.computeIfAbsent(key(l.item), k -> new ArrayList<>()).add(l);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<String, Rate> out = new LinkedHashMap<>();
|
||||||
|
for (Map.Entry<String, List<DropLine>> e : byItem.entrySet())
|
||||||
|
{
|
||||||
|
List<DropLine> ls = e.getValue();
|
||||||
|
double hits = 0;
|
||||||
|
for (DropLine l : ls)
|
||||||
|
{
|
||||||
|
hits += l.hitsPerKill();
|
||||||
|
}
|
||||||
|
String text = ls.size() == 1 || Double.isNaN(hits) ? ls.get(0).rarityText : formatChance(hits);
|
||||||
|
out.put(e.getKey(), new Rate(ls.get(0).item, text, hits));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String formatChance(double perKill)
|
||||||
|
{
|
||||||
|
if (perKill >= 1)
|
||||||
|
{
|
||||||
|
return "Always";
|
||||||
|
}
|
||||||
|
if (perKill <= 0)
|
||||||
|
{
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
double inv = 1 / perKill;
|
||||||
|
return inv >= 100 ? String.format(Locale.ROOT, "1/%,d", Math.round(inv)) : String.format(Locale.ROOT, "1/%.1f", inv);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String key(String name)
|
||||||
|
{
|
||||||
|
return name.trim().toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
package com.example.essentialsplus.loot.gear;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/** How an attack is made: attack type, stance and (for magic) the spell or powered staff. */
|
||||||
|
public final class CombatChoice
|
||||||
|
{
|
||||||
|
public enum Type
|
||||||
|
{
|
||||||
|
STAB("Stab"), SLASH("Slash"), CRUSH("Crush"), RANGED("Ranged"), MAGIC("Magic");
|
||||||
|
|
||||||
|
public final String label;
|
||||||
|
|
||||||
|
Type(String label)
|
||||||
|
{
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean melee()
|
||||||
|
{
|
||||||
|
return this == STAB || this == SLASH || this == CRUSH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Stance
|
||||||
|
{
|
||||||
|
ACCURATE("Accurate"), AGGRESSIVE("Aggressive"), CONTROLLED("Controlled"), DEFENSIVE("Defensive"),
|
||||||
|
RAPID("Rapid"), LONGRANGE("Longrange"), CASTING("Autocast"), MANUAL("Manual cast");
|
||||||
|
|
||||||
|
public final String label;
|
||||||
|
|
||||||
|
Stance(String label)
|
||||||
|
{
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A castable spell: base max hit (-1 when it scales with the magic level) and element. */
|
||||||
|
public static final class Spell
|
||||||
|
{
|
||||||
|
public final String name;
|
||||||
|
public final int base;
|
||||||
|
public final String element;
|
||||||
|
|
||||||
|
Spell(String name, int base, String element)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
this.base = base;
|
||||||
|
this.element = element;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final Type type;
|
||||||
|
public final Stance stance;
|
||||||
|
/** null when not magic or when a powered staff attacks on its own */
|
||||||
|
public final Spell spell;
|
||||||
|
|
||||||
|
public CombatChoice(Type type, Stance stance, Spell spell)
|
||||||
|
{
|
||||||
|
this.type = type;
|
||||||
|
this.stance = stance;
|
||||||
|
this.spell = spell;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String label()
|
||||||
|
{
|
||||||
|
if (type == Type.MAGIC)
|
||||||
|
{
|
||||||
|
return spell != null ? spell.name : "Powered staff";
|
||||||
|
}
|
||||||
|
return type.label + " · " + stance.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
// COM_MODE button -> stance per weapon category (varbit COMBAT_WEAPON_CATEGORY), null = no button
|
||||||
|
private static final Stance A = Stance.ACCURATE, G = Stance.AGGRESSIVE, C = Stance.CONTROLLED, D = Stance.DEFENSIVE,
|
||||||
|
R = Stance.RAPID, L = Stance.LONGRANGE;
|
||||||
|
private static final Type ST = Type.STAB, SL = Type.SLASH, CR = Type.CRUSH, RA = Type.RANGED;
|
||||||
|
|
||||||
|
private static final Object[][][] CATEGORIES = {
|
||||||
|
/* 0 unarmed */ {{CR, A}, {CR, G}, null, {CR, D}},
|
||||||
|
/* 1 axe */ {{SL, A}, {SL, G}, {CR, G}, {SL, D}},
|
||||||
|
/* 2 blunt */ {{CR, A}, {CR, G}, null, {CR, D}},
|
||||||
|
/* 3 bow */ {{RA, A}, {RA, R}, null, {RA, L}},
|
||||||
|
/* 4 claw */ {{SL, A}, {SL, G}, {ST, C}, {SL, D}},
|
||||||
|
/* 5 crossbow */ {{RA, A}, {RA, R}, null, {RA, L}},
|
||||||
|
/* 6 salamander */ {{SL, G}, {RA, A}, null, null},
|
||||||
|
/* 7 chinchompa */ {{RA, A}, {RA, R}, null, {RA, L}},
|
||||||
|
/* 8 gun */ null,
|
||||||
|
/* 9 slash sword */ {{SL, A}, {SL, G}, {ST, C}, {SL, D}},
|
||||||
|
/* 10 2h sword */ {{SL, A}, {SL, G}, {CR, G}, {SL, D}},
|
||||||
|
/* 11 pickaxe */ {{ST, A}, {ST, G}, {CR, G}, {ST, D}},
|
||||||
|
/* 12 halberd */ {{ST, C}, {SL, G}, null, {ST, D}},
|
||||||
|
/* 13 polestaff */ {{CR, A}, {CR, G}, null, {CR, D}},
|
||||||
|
/* 14 scythe */ {{SL, A}, {SL, G}, {CR, G}, {SL, D}},
|
||||||
|
/* 15 spear */ {{ST, C}, {SL, C}, {CR, C}, {ST, D}},
|
||||||
|
/* 16 spiked */ {{CR, A}, {CR, G}, {ST, C}, {CR, D}},
|
||||||
|
/* 17 stab sword */ {{ST, A}, {ST, G}, {SL, G}, {ST, D}},
|
||||||
|
/* 18 staff */ {{CR, A}, {CR, G}, null, {CR, D}},
|
||||||
|
/* 19 thrown */ {{RA, A}, {RA, R}, null, {RA, L}},
|
||||||
|
/* 20 whip */ {{SL, A}, {SL, C}, null, {SL, D}},
|
||||||
|
/* 21 bladed staff */ {{ST, A}, {SL, G}, null, {CR, D}},
|
||||||
|
/* 22 */ null,
|
||||||
|
/* 23 powered staff */ null,
|
||||||
|
/* 24 banner */ {{ST, A}, {SL, G}, {CR, C}, {ST, D}},
|
||||||
|
/* 25 polearm */ {{ST, C}, {SL, G}, null, {ST, D}},
|
||||||
|
/* 26 bludgeon */ {{CR, G}, {CR, G}, null, {CR, G}},
|
||||||
|
/* 27 bulwark */ {{CR, A}, null, null, null},
|
||||||
|
/* 28 */ null,
|
||||||
|
/* 29 partisan */ {{ST, A}, {ST, G}, {CR, G}, {ST, D}},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The attack style the player has selected for the worn weapon, or null if the category is not known. */
|
||||||
|
public static CombatChoice fromVarbits(int category, int comMode, int autocastSpell, int magicLevel, GearItem weapon)
|
||||||
|
{
|
||||||
|
if (category == 23 || isPoweredStaff(weapon))
|
||||||
|
{
|
||||||
|
return new CombatChoice(Type.MAGIC, Stance.CASTING, null);
|
||||||
|
}
|
||||||
|
if ((category == 18 || category == 21) && comMode >= 4)
|
||||||
|
{
|
||||||
|
Spell spell = spell(autocastSpell);
|
||||||
|
return new CombatChoice(Type.MAGIC, Stance.CASTING, spell != null ? spell : bestStandardSpell(magicLevel));
|
||||||
|
}
|
||||||
|
if (category < 0 || category >= CATEGORIES.length || CATEGORIES[category] == null
|
||||||
|
|| comMode < 0 || comMode > 3 || CATEGORIES[category][comMode] == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Object[] entry = CATEGORIES[category][comMode];
|
||||||
|
return new CombatChoice((Type) entry[0], (Stance) entry[1], null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Best guess for a weapon that is not worn: its main attack type with the damage stance. */
|
||||||
|
public static CombatChoice infer(GearItem weapon, int magicLevel, int autocastSpell)
|
||||||
|
{
|
||||||
|
switch (weaponClass(weapon))
|
||||||
|
{
|
||||||
|
case RANGED:
|
||||||
|
return new CombatChoice(Type.RANGED, Stance.RAPID, null);
|
||||||
|
case MAGIC:
|
||||||
|
if (isPoweredStaff(weapon))
|
||||||
|
{
|
||||||
|
return new CombatChoice(Type.MAGIC, Stance.CASTING, null);
|
||||||
|
}
|
||||||
|
Spell s = spell(autocastSpell);
|
||||||
|
return new CombatChoice(Type.MAGIC, Stance.CASTING, s != null ? s : bestStandardSpell(magicLevel));
|
||||||
|
default:
|
||||||
|
return new CombatChoice(bestMeleeType(weapon), Stance.AGGRESSIVE, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Melee choice of a weapon for one attack type (optimizer): aggressive when possible. */
|
||||||
|
public static CombatChoice melee(Type type)
|
||||||
|
{
|
||||||
|
return new CombatChoice(type, Stance.AGGRESSIVE, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Type bestMeleeType(GearItem w)
|
||||||
|
{
|
||||||
|
if (w == null)
|
||||||
|
{
|
||||||
|
return Type.CRUSH;
|
||||||
|
}
|
||||||
|
if (w.astab >= w.aslash && w.astab >= w.acrush)
|
||||||
|
{
|
||||||
|
return Type.STAB;
|
||||||
|
}
|
||||||
|
return w.aslash >= w.acrush ? Type.SLASH : Type.CRUSH;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** RANGED, MAGIC or melee (STAB) class of a weapon. */
|
||||||
|
public static Type weaponClass(GearItem w)
|
||||||
|
{
|
||||||
|
if (w == null)
|
||||||
|
{
|
||||||
|
return Type.STAB;
|
||||||
|
}
|
||||||
|
if (isPoweredStaff(w))
|
||||||
|
{
|
||||||
|
return Type.MAGIC;
|
||||||
|
}
|
||||||
|
boolean rangedName = w.has("bow") || w.has("dart") || w.has("knife") || w.has("thrownaxe") || w.has("chinchompa")
|
||||||
|
|| w.has("ballista") || w.has("blowpipe") || w.has("javelin") || w.has("atlatl") || w.has("tonalztics");
|
||||||
|
int meleeMax = Math.max(w.astab, Math.max(w.aslash, w.acrush));
|
||||||
|
if (rangedName && w.arange > 0 || w.arange > 0 && w.arange > meleeMax && w.amagic <= 0)
|
||||||
|
{
|
||||||
|
return Type.RANGED;
|
||||||
|
}
|
||||||
|
boolean magicName = w.has("staff") || w.has("wand") || w.has("sceptre") || w.has("trident");
|
||||||
|
if (magicName && w.amagic >= meleeMax)
|
||||||
|
{
|
||||||
|
return Type.MAGIC;
|
||||||
|
}
|
||||||
|
return Type.STAB;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isPoweredStaff(GearItem w)
|
||||||
|
{
|
||||||
|
return poweredBase(w, 99) != Integer.MIN_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Base max hit of a powered staff at the given (boosted) magic level, MIN_VALUE if not one. */
|
||||||
|
public static int poweredBase(GearItem w, int magic)
|
||||||
|
{
|
||||||
|
if (w == null)
|
||||||
|
{
|
||||||
|
return Integer.MIN_VALUE;
|
||||||
|
}
|
||||||
|
String n = w.lower;
|
||||||
|
if (n.startsWith("trident of the seas"))
|
||||||
|
{
|
||||||
|
return magic / 3 - 5;
|
||||||
|
}
|
||||||
|
if (n.startsWith("trident of the swamp"))
|
||||||
|
{
|
||||||
|
return magic / 3 - 2;
|
||||||
|
}
|
||||||
|
if (n.startsWith("sanguinesti staff") || n.startsWith("holy sanguinesti staff"))
|
||||||
|
{
|
||||||
|
return magic / 3 - 1;
|
||||||
|
}
|
||||||
|
if (n.startsWith("tumeken's shadow"))
|
||||||
|
{
|
||||||
|
return magic / 3 + 1;
|
||||||
|
}
|
||||||
|
if (n.startsWith("thammaron's sceptre"))
|
||||||
|
{
|
||||||
|
return magic / 3 - 8;
|
||||||
|
}
|
||||||
|
if (n.startsWith("accursed sceptre"))
|
||||||
|
{
|
||||||
|
return magic / 3 - 6;
|
||||||
|
}
|
||||||
|
if (n.startsWith("warped sceptre"))
|
||||||
|
{
|
||||||
|
return (8 * magic + 96) / 37;
|
||||||
|
}
|
||||||
|
if (n.startsWith("crystal staff (basic)") || n.startsWith("corrupted staff (basic)"))
|
||||||
|
{
|
||||||
|
return 23;
|
||||||
|
}
|
||||||
|
if (n.startsWith("crystal staff (attuned)") || n.startsWith("corrupted staff (attuned)"))
|
||||||
|
{
|
||||||
|
return 31;
|
||||||
|
}
|
||||||
|
if (n.startsWith("crystal staff (perfected)") || n.startsWith("corrupted staff (perfected)"))
|
||||||
|
{
|
||||||
|
return 39;
|
||||||
|
}
|
||||||
|
return Integer.MIN_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Spell of varbit AUTOCAST_SPELL, null for none or unknown. */
|
||||||
|
public static Spell spell(int autocast)
|
||||||
|
{
|
||||||
|
switch (autocast)
|
||||||
|
{
|
||||||
|
case 1: return new Spell("Wind Strike", 2, "Air");
|
||||||
|
case 2: return new Spell("Water Strike", 4, "Water");
|
||||||
|
case 3: return new Spell("Earth Strike", 6, "Earth");
|
||||||
|
case 4: return new Spell("Fire Strike", 8, "Fire");
|
||||||
|
case 5: return new Spell("Wind Bolt", 9, "Air");
|
||||||
|
case 6: return new Spell("Water Bolt", 10, "Water");
|
||||||
|
case 7: return new Spell("Earth Bolt", 11, "Earth");
|
||||||
|
case 8: return new Spell("Fire Bolt", 12, "Fire");
|
||||||
|
case 9: return new Spell("Wind Blast", 13, "Air");
|
||||||
|
case 10: return new Spell("Water Blast", 14, "Water");
|
||||||
|
case 11: return new Spell("Earth Blast", 15, "Earth");
|
||||||
|
case 12: return new Spell("Fire Blast", 16, "Fire");
|
||||||
|
case 13: return new Spell("Wind Wave", 17, "Air");
|
||||||
|
case 14: return new Spell("Water Wave", 18, "Water");
|
||||||
|
case 15: return new Spell("Earth Wave", 19, "Earth");
|
||||||
|
case 16: return new Spell("Fire Wave", 20, "Fire");
|
||||||
|
case 17: return new Spell("Crumble Undead", 15, null);
|
||||||
|
case 18: return new Spell("Magic Dart", -1, null);
|
||||||
|
case 19: return new Spell("Claws of Guthix", 20, null);
|
||||||
|
case 20: return new Spell("Flames of Zamorak", 20, null);
|
||||||
|
case 21: return new Spell("Saradomin Strike", 20, null);
|
||||||
|
case 31: return new Spell("Smoke Rush", 13, null);
|
||||||
|
case 32: return new Spell("Shadow Rush", 14, null);
|
||||||
|
case 33: return new Spell("Blood Rush", 15, null);
|
||||||
|
case 34: return new Spell("Ice Rush", 16, null);
|
||||||
|
case 35: return new Spell("Smoke Burst", 17, null);
|
||||||
|
case 36: return new Spell("Shadow Burst", 18, null);
|
||||||
|
case 37: return new Spell("Blood Burst", 21, null);
|
||||||
|
case 38: return new Spell("Ice Burst", 22, null);
|
||||||
|
case 39: return new Spell("Smoke Blitz", 23, null);
|
||||||
|
case 40: return new Spell("Shadow Blitz", 24, null);
|
||||||
|
case 41: return new Spell("Blood Blitz", 25, null);
|
||||||
|
case 42: return new Spell("Ice Blitz", 26, null);
|
||||||
|
case 43: return new Spell("Smoke Barrage", 27, null);
|
||||||
|
case 44: return new Spell("Shadow Barrage", 28, null);
|
||||||
|
case 45: return new Spell("Blood Barrage", 29, null);
|
||||||
|
case 46: return new Spell("Ice Barrage", 30, null);
|
||||||
|
case 47: return new Spell("Iban Blast", 25, null);
|
||||||
|
case 48: return new Spell("Wind Surge", 21, "Air");
|
||||||
|
case 49: return new Spell("Water Surge", 22, "Water");
|
||||||
|
case 50: return new Spell("Earth Surge", 23, "Earth");
|
||||||
|
case 51: return new Spell("Fire Surge", 24, "Fire");
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Strongest fire spell of the standard spellbook at a magic level. */
|
||||||
|
public static Spell bestStandardSpell(int magic)
|
||||||
|
{
|
||||||
|
if (magic >= 95) return spell(51);
|
||||||
|
if (magic >= 75) return spell(16);
|
||||||
|
if (magic >= 59) return spell(12);
|
||||||
|
if (magic >= 35) return spell(8);
|
||||||
|
if (magic >= 13) return spell(4);
|
||||||
|
return spell(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return label().toLowerCase(Locale.ENGLISH);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,333 @@
|
|||||||
|
package com.example.essentialsplus.loot.gear;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.loot.gear.CombatChoice.Stance;
|
||||||
|
import com.example.essentialsplus.loot.gear.CombatChoice.Type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Max hit, accuracy and DPS with the standard OSRS formulas. Covers prayers, stances, void, salve, slayer helm,
|
||||||
|
* dragon hunter, arclight, twisted bow, crystal armour, powered staves and elemental weaknesses; other special
|
||||||
|
* effects (special attacks, set effects, multi-hit weapons) are left out, so it is an estimate.
|
||||||
|
*/
|
||||||
|
public final class DpsCalc
|
||||||
|
{
|
||||||
|
private DpsCalc()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Result
|
||||||
|
{
|
||||||
|
public final CombatChoice choice;
|
||||||
|
public final int maxHit;
|
||||||
|
public final double hitChance;
|
||||||
|
public final double dps;
|
||||||
|
public final int speed;
|
||||||
|
public final long attackRoll;
|
||||||
|
public final long defenceRoll;
|
||||||
|
|
||||||
|
Result(CombatChoice choice, int maxHit, double hitChance, int speed, long attackRoll, long defenceRoll)
|
||||||
|
{
|
||||||
|
this.choice = choice;
|
||||||
|
this.maxHit = Math.max(0, maxHit);
|
||||||
|
this.hitChance = hitChance;
|
||||||
|
this.speed = Math.max(1, speed);
|
||||||
|
this.attackRoll = attackRoll;
|
||||||
|
this.defenceRoll = defenceRoll;
|
||||||
|
this.dps = hitChance * this.maxHit / 2.0 / (this.speed * 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Expected seconds to kill {@code hp}, NaN if no damage. */
|
||||||
|
public double secondsToKill(int hp)
|
||||||
|
{
|
||||||
|
return dps > 0 && hp > 0 ? hp / dps : Double.NaN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Uses the selected style when the worn weapon is still worn, otherwise the weapon's likely style. */
|
||||||
|
public static CombatChoice choiceFor(Loadout loadout, Loadout worn, PlayerSetup p)
|
||||||
|
{
|
||||||
|
GearItem w = loadout.weapon();
|
||||||
|
if (loadout.id(GearItem.WEAPON) == worn.id(GearItem.WEAPON))
|
||||||
|
{
|
||||||
|
CombatChoice live = CombatChoice.fromVarbits(p.weaponCategory, p.comMode, p.autocastSpell, p.magic, w);
|
||||||
|
if (live != null)
|
||||||
|
{
|
||||||
|
return live;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CombatChoice.infer(w, p.magic, p.autocastSpell);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result calc(Loadout l, PlayerSetup p, Monster m, CombatChoice c)
|
||||||
|
{
|
||||||
|
switch (c.type)
|
||||||
|
{
|
||||||
|
case RANGED:
|
||||||
|
return ranged(l, p, m, c);
|
||||||
|
case MAGIC:
|
||||||
|
return magic(l, p, m, c);
|
||||||
|
default:
|
||||||
|
return melee(l, p, m, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static double hitChance(long att, long def)
|
||||||
|
{
|
||||||
|
if (att <= 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (att > def)
|
||||||
|
{
|
||||||
|
return 1.0 - (def + 2.0) / (2.0 * (att + 1.0));
|
||||||
|
}
|
||||||
|
return att / (2.0 * (def + 1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long defenceRoll(Monster m, int level, int bonus)
|
||||||
|
{
|
||||||
|
return (long) (level + 9) * (bonus + 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int weaponSpeed(Loadout l)
|
||||||
|
{
|
||||||
|
GearItem w = l.weapon();
|
||||||
|
return w == null || w.aspeed <= 0 ? 4 : w.aspeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Result melee(Loadout l, PlayerSetup p, Monster m, CombatChoice c)
|
||||||
|
{
|
||||||
|
int[] b = l.bonuses();
|
||||||
|
int attBonus = c.type == Type.STAB ? b[0] : c.type == Type.SLASH ? b[1] : b[2];
|
||||||
|
int stanceAtt = c.stance == Stance.ACCURATE ? 3 : c.stance == Stance.CONTROLLED ? 1 : 0;
|
||||||
|
int stanceStr = c.stance == Stance.AGGRESSIVE ? 3 : c.stance == Stance.CONTROLLED ? 1 : 0;
|
||||||
|
|
||||||
|
int effAtt = (int) Math.floor(p.attack * p.meleeAcc) + stanceAtt + 8;
|
||||||
|
int effStr = (int) Math.floor(p.strength * p.meleeStr) + stanceStr + 8;
|
||||||
|
if (hasVoid(l, "melee"))
|
||||||
|
{
|
||||||
|
effAtt = effAtt * 11 / 10;
|
||||||
|
effStr = effStr * 11 / 10;
|
||||||
|
}
|
||||||
|
long att = (long) effAtt * (attBonus + 64);
|
||||||
|
int max = (effStr * (b[10] + 64) + 320) / 640;
|
||||||
|
|
||||||
|
// salve and slayer helm do not stack; salve wins
|
||||||
|
if (m.hasAttribute("undead") && l.slotHas(GearItem.AMULET, "salve amulet"))
|
||||||
|
{
|
||||||
|
boolean e = l.slotHas(GearItem.AMULET, "(e") ;
|
||||||
|
att = e ? att * 6 / 5 : att * 7 / 6;
|
||||||
|
max = e ? max * 6 / 5 : max * 7 / 6;
|
||||||
|
}
|
||||||
|
else if (p.onSlayerTask && (l.slotHas(GearItem.HEAD, "slayer helmet") || l.slotHas(GearItem.HEAD, "black mask")))
|
||||||
|
{
|
||||||
|
att = att * 7 / 6;
|
||||||
|
max = max * 7 / 6;
|
||||||
|
}
|
||||||
|
GearItem w = l.weapon();
|
||||||
|
if (w != null && m.hasAttribute("dragon") && w.has("dragon hunter lance"))
|
||||||
|
{
|
||||||
|
att = att * 6 / 5;
|
||||||
|
max = max * 6 / 5;
|
||||||
|
}
|
||||||
|
if (w != null && m.hasAttribute("demon") && (w.has("arclight") || w.has("emberlight")))
|
||||||
|
{
|
||||||
|
att = att * 17 / 10;
|
||||||
|
max = max * 17 / 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
int defBonus = c.type == Type.STAB ? m.dstab : c.type == Type.SLASH ? m.dslash : m.dcrush;
|
||||||
|
long def = defenceRoll(m, m.defenceLevel, defBonus);
|
||||||
|
return new Result(c, max, hitChance(att, def), weaponSpeed(l), att, def);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ammo type the weapon fires from the ammo slot: "arrow", "bolt", "javelin" or null (no ammo used). */
|
||||||
|
static String ammoFor(GearItem w)
|
||||||
|
{
|
||||||
|
if (w == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (w.has("ballista"))
|
||||||
|
{
|
||||||
|
return "javelin";
|
||||||
|
}
|
||||||
|
if (w.has("crossbow") || w.has("karil"))
|
||||||
|
{
|
||||||
|
return w.has("karil") ? "bolt rack" : "bolt";
|
||||||
|
}
|
||||||
|
if (w.has("bow") && !w.has("crystal bow") && !w.has("bow of faerdhinen") && !w.has("blowpipe"))
|
||||||
|
{
|
||||||
|
return "arrow";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Result ranged(Loadout l, PlayerSetup p, Monster m, CombatChoice c)
|
||||||
|
{
|
||||||
|
int[] b = l.bonuses();
|
||||||
|
GearItem w = l.weapon();
|
||||||
|
GearItem ammo = l.get(GearItem.AMMO);
|
||||||
|
int rstr = b[11];
|
||||||
|
if (ammo != null)
|
||||||
|
{
|
||||||
|
String wanted = ammoFor(w);
|
||||||
|
if (wanted == null || !ammo.has(wanted))
|
||||||
|
{
|
||||||
|
// ammo the weapon cannot fire adds no ranged strength
|
||||||
|
rstr -= ammo.rstr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int stance = c.stance == Stance.ACCURATE ? 3 : 0;
|
||||||
|
int effAtt = (int) Math.floor(p.ranged * p.rangedAcc) + stance + 8;
|
||||||
|
int effStr = (int) Math.floor(p.ranged * p.rangedStr) + stance + 8;
|
||||||
|
if (hasVoid(l, "ranger"))
|
||||||
|
{
|
||||||
|
effAtt = effAtt * 11 / 10;
|
||||||
|
effStr = isEliteVoid(l) ? effStr * 9 / 8 : effStr * 11 / 10;
|
||||||
|
}
|
||||||
|
long att = (long) effAtt * (b[4] + 64);
|
||||||
|
int max = (effStr * (rstr + 64) + 320) / 640;
|
||||||
|
|
||||||
|
if (w != null && (w.has("crystal bow") || w.has("bow of faerdhinen")))
|
||||||
|
{
|
||||||
|
int accPct = 0;
|
||||||
|
int dmgPct = 0;
|
||||||
|
if (l.slotHas(GearItem.HEAD, "crystal helm"))
|
||||||
|
{
|
||||||
|
accPct += 5;
|
||||||
|
dmgPct += 5;
|
||||||
|
}
|
||||||
|
if (l.slotHas(GearItem.BODY, "crystal body"))
|
||||||
|
{
|
||||||
|
accPct += 15;
|
||||||
|
dmgPct += 15;
|
||||||
|
}
|
||||||
|
if (l.slotHas(GearItem.LEGS, "crystal legs"))
|
||||||
|
{
|
||||||
|
accPct += 10;
|
||||||
|
dmgPct += 10;
|
||||||
|
}
|
||||||
|
att = att * (200 + accPct) / 200;
|
||||||
|
max = max * (200 + dmgPct) / 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean imbued = l.slotHas(GearItem.AMULET, "(i)") || l.slotHas(GearItem.AMULET, "(ei)");
|
||||||
|
if (m.hasAttribute("undead") && l.slotHas(GearItem.AMULET, "salve amulet") && imbued)
|
||||||
|
{
|
||||||
|
boolean e = l.slotHas(GearItem.AMULET, "(ei)");
|
||||||
|
att = e ? att * 6 / 5 : att * 7 / 6;
|
||||||
|
max = e ? max * 6 / 5 : max * 7 / 6;
|
||||||
|
}
|
||||||
|
else if (p.onSlayerTask && (l.slotHas(GearItem.HEAD, "slayer helmet (i)") || l.slotHas(GearItem.HEAD, "black mask (i)")))
|
||||||
|
{
|
||||||
|
att = att * 23 / 20;
|
||||||
|
max = max * 23 / 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (w != null && w.has("twisted bow"))
|
||||||
|
{
|
||||||
|
int mg = Math.min(250, Math.max(m.magicLevel, m.magicAttack));
|
||||||
|
int accPct = Math.min(140, 140 + (3 * mg - 10) / 100 - (int) Math.pow(3 * mg / 10 - 100, 2) / 100);
|
||||||
|
int dmgPct = Math.min(250, 250 + (3 * mg - 14) / 100 - (int) Math.pow(3 * mg / 10 - 140, 2) / 100);
|
||||||
|
att = att * Math.max(0, accPct) / 100;
|
||||||
|
max = max * Math.max(0, dmgPct) / 100;
|
||||||
|
}
|
||||||
|
if (w != null && m.hasAttribute("dragon") && w.has("dragon hunter crossbow"))
|
||||||
|
{
|
||||||
|
att = att * 13 / 10;
|
||||||
|
max = max * 5 / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
int defBonus;
|
||||||
|
if (w != null && (w.has("crossbow") || w.has("ballista") || w.has("karil")))
|
||||||
|
{
|
||||||
|
defBonus = m.dheavy;
|
||||||
|
}
|
||||||
|
else if (w != null && (w.has("dart") || w.has("knife") || w.has("thrownaxe") || w.has("blowpipe") || w.has("chinchompa")))
|
||||||
|
{
|
||||||
|
defBonus = m.dlight;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
defBonus = m.dstandard;
|
||||||
|
}
|
||||||
|
long def = defenceRoll(m, m.defenceLevel, defBonus);
|
||||||
|
int speed = weaponSpeed(l) - (c.stance == Stance.RAPID ? 1 : 0);
|
||||||
|
return new Result(c, max, hitChance(att, def), speed, att, def);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Result magic(Loadout l, PlayerSetup p, Monster m, CombatChoice c)
|
||||||
|
{
|
||||||
|
int[] b = l.bonuses();
|
||||||
|
GearItem w = l.weapon();
|
||||||
|
int base;
|
||||||
|
int speed;
|
||||||
|
String element = null;
|
||||||
|
if (c.spell == null)
|
||||||
|
{
|
||||||
|
base = CombatChoice.poweredBase(w, p.magic);
|
||||||
|
speed = weaponSpeed(l);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
base = c.spell.base >= 0 ? c.spell.base : 10 + p.magic / 10;
|
||||||
|
element = c.spell.element;
|
||||||
|
speed = 5;
|
||||||
|
}
|
||||||
|
if (base == Integer.MIN_VALUE || base <= 0)
|
||||||
|
{
|
||||||
|
return new Result(c, 0, 0, speed, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean shadow = w != null && w.has("tumeken's shadow") && c.spell == null;
|
||||||
|
int magicAttack = shadow ? b[3] * 3 : b[3];
|
||||||
|
double dmgPct = (shadow ? l.magicDamage() * 3 : l.magicDamage()) + p.magicDmgPct;
|
||||||
|
|
||||||
|
int effAtt = (int) Math.floor(p.magic * p.magicAcc) + 9;
|
||||||
|
if (hasVoid(l, "mage"))
|
||||||
|
{
|
||||||
|
effAtt = effAtt * 29 / 20;
|
||||||
|
if (isEliteVoid(l))
|
||||||
|
{
|
||||||
|
dmgPct += 2.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
long att = (long) effAtt * (magicAttack + 64);
|
||||||
|
int max = (int) Math.floor(base * (1 + dmgPct / 100.0));
|
||||||
|
|
||||||
|
boolean imbued = l.slotHas(GearItem.AMULET, "(i)") || l.slotHas(GearItem.AMULET, "(ei)");
|
||||||
|
if (m.hasAttribute("undead") && l.slotHas(GearItem.AMULET, "salve amulet") && imbued)
|
||||||
|
{
|
||||||
|
boolean e = l.slotHas(GearItem.AMULET, "(ei)");
|
||||||
|
att = e ? att * 6 / 5 : att * 23 / 20;
|
||||||
|
max = e ? max * 6 / 5 : max * 23 / 20;
|
||||||
|
}
|
||||||
|
else if (p.onSlayerTask && (l.slotHas(GearItem.HEAD, "slayer helmet (i)") || l.slotHas(GearItem.HEAD, "black mask (i)")))
|
||||||
|
{
|
||||||
|
att = att * 23 / 20;
|
||||||
|
max = max * 23 / 20;
|
||||||
|
}
|
||||||
|
if (element != null && m.elementalWeakness != null && element.equalsIgnoreCase(m.elementalWeakness) && m.elementalWeaknessPct > 0)
|
||||||
|
{
|
||||||
|
att = att * (100 + m.elementalWeaknessPct) / 100;
|
||||||
|
max = max + base * m.elementalWeaknessPct / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
long def = defenceRoll(m, m.magicLevel, m.dmagic);
|
||||||
|
return new Result(c, max, hitChance(att, def), speed, att, def);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasVoid(Loadout l, String helm)
|
||||||
|
{
|
||||||
|
return l.slotHas(GearItem.HEAD, "void " + helm + " helm")
|
||||||
|
&& (l.slotHas(GearItem.BODY, "void knight top") || l.slotHas(GearItem.BODY, "elite void top"))
|
||||||
|
&& (l.slotHas(GearItem.LEGS, "void knight robe") || l.slotHas(GearItem.LEGS, "elite void robe"))
|
||||||
|
&& l.slotHas(GearItem.GLOVES, "void knight gloves");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isEliteVoid(Loadout l)
|
||||||
|
{
|
||||||
|
return l.slotHas(GearItem.BODY, "elite void top") && l.slotHas(GearItem.LEGS, "elite void robe");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,492 @@
|
|||||||
|
package com.example.essentialsplus.loot.gear;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.helpers.AttackStyles;
|
||||||
|
import com.example.essentialsplus.helpers.CombatStyle;
|
||||||
|
import com.example.essentialsplus.loot.combat.CombatTracker;
|
||||||
|
import com.example.essentialsplus.loot.combat.FightSession;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import net.runelite.api.Actor;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.GameState;
|
||||||
|
import net.runelite.api.Item;
|
||||||
|
import net.runelite.api.ItemComposition;
|
||||||
|
import net.runelite.api.ItemContainer;
|
||||||
|
import net.runelite.api.NPC;
|
||||||
|
import net.runelite.api.Player;
|
||||||
|
import net.runelite.api.Prayer;
|
||||||
|
import net.runelite.api.Skill;
|
||||||
|
import net.runelite.api.gameval.InventoryID;
|
||||||
|
import net.runelite.api.gameval.VarPlayerID;
|
||||||
|
import net.runelite.api.gameval.VarbitID;
|
||||||
|
import net.runelite.client.game.ItemManager;
|
||||||
|
import net.runelite.client.game.ItemStats;
|
||||||
|
import net.runelite.client.util.Text;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds {@link FightSnapshot}s on the client thread: worn gear, owned equipment (worn, inventory, bank),
|
||||||
|
* levels/prayers/style, the current target with its wiki stats and the running fight session.
|
||||||
|
* The best-in-bank search runs on the background executor whenever gear or target change.
|
||||||
|
*/
|
||||||
|
public class FightService
|
||||||
|
{
|
||||||
|
private final Client client;
|
||||||
|
private final ItemManager itemManager;
|
||||||
|
private final CombatTracker combatTracker;
|
||||||
|
private final AttackStyles attackStyles;
|
||||||
|
private final MonsterService monsters;
|
||||||
|
private final Executor executor;
|
||||||
|
/** saved bank of the logged-in account (item id -> quantity), may be null */
|
||||||
|
private final Supplier<Map<Integer, Long>> bank;
|
||||||
|
private final List<Consumer<FightSnapshot>> listeners = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
|
/** base item (source WORN) per canonical id, null value = not equipable */
|
||||||
|
private final Map<Integer, GearItem> itemCache = new java.util.HashMap<>();
|
||||||
|
|
||||||
|
private volatile boolean slayerTask;
|
||||||
|
private volatile boolean monsterLoaded;
|
||||||
|
private volatile boolean clearRequested;
|
||||||
|
private boolean containersChanged = true;
|
||||||
|
|
||||||
|
private PlayerSetup lastSetup;
|
||||||
|
private int[] lastWornIds;
|
||||||
|
private Loadout lastWorn;
|
||||||
|
private List<GearItem> pool = Collections.emptyList();
|
||||||
|
private long poolHash;
|
||||||
|
private int targetNpcIndex = -1;
|
||||||
|
private FightSnapshot.Target lastTarget;
|
||||||
|
private Monster lastMonster;
|
||||||
|
private volatile long gearVersion;
|
||||||
|
private volatile List<GearOptimizer.Option> best = Collections.emptyList();
|
||||||
|
private volatile long bestVersion = -1;
|
||||||
|
private volatile boolean bestReady;
|
||||||
|
private FightSnapshot last = FightSnapshot.LOGGED_OUT;
|
||||||
|
|
||||||
|
public FightService(Client client, ItemManager itemManager, CombatTracker combatTracker, AttackStyles attackStyles,
|
||||||
|
MonsterService monsters, Executor executor, Supplier<Map<Integer, Long>> bank)
|
||||||
|
{
|
||||||
|
this.client = client;
|
||||||
|
this.itemManager = itemManager;
|
||||||
|
this.combatTracker = combatTracker;
|
||||||
|
this.attackStyles = attackStyles;
|
||||||
|
this.monsters = monsters;
|
||||||
|
this.executor = executor;
|
||||||
|
this.bank = bank;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addListener(Consumer<FightSnapshot> l)
|
||||||
|
{
|
||||||
|
listeners.add(l);
|
||||||
|
l.accept(last);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeListener(Consumer<FightSnapshot> l)
|
||||||
|
{
|
||||||
|
listeners.remove(l);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FightSnapshot last()
|
||||||
|
{
|
||||||
|
return last;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSlayerTask()
|
||||||
|
{
|
||||||
|
return slayerTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Slayer helmet / black mask bonus applies (set from the panel). */
|
||||||
|
public void setSlayerTask(boolean task)
|
||||||
|
{
|
||||||
|
slayerTask = task;
|
||||||
|
lastSetup = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called from the wiki thread when monster stats arrive. */
|
||||||
|
public void onMonsterLoaded()
|
||||||
|
{
|
||||||
|
monsterLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onContainerChanged()
|
||||||
|
{
|
||||||
|
containersChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Forgets the last target (panel "clear"). Any thread. */
|
||||||
|
public void clearTarget()
|
||||||
|
{
|
||||||
|
clearRequested = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset()
|
||||||
|
{
|
||||||
|
lastSetup = null;
|
||||||
|
lastWornIds = null;
|
||||||
|
lastWorn = null;
|
||||||
|
lastTarget = null;
|
||||||
|
lastMonster = null;
|
||||||
|
targetNpcIndex = -1;
|
||||||
|
pool = Collections.emptyList();
|
||||||
|
poolHash = 0;
|
||||||
|
containersChanged = true;
|
||||||
|
itemCache.clear();
|
||||||
|
publish(FightSnapshot.LOGGED_OUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onGameTick()
|
||||||
|
{
|
||||||
|
Player me = client.getLocalPlayer();
|
||||||
|
if (client.getGameState() != GameState.LOGGED_IN || me == null)
|
||||||
|
{
|
||||||
|
if (last.loggedIn)
|
||||||
|
{
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean gearChanged = false;
|
||||||
|
|
||||||
|
PlayerSetup setup = readSetup();
|
||||||
|
if (lastSetup == null || !sameSetup(setup, lastSetup))
|
||||||
|
{
|
||||||
|
lastSetup = setup;
|
||||||
|
gearChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Loadout worn = lastWorn;
|
||||||
|
if (containersChanged || worn == null)
|
||||||
|
{
|
||||||
|
worn = readWorn();
|
||||||
|
int[] ids = new int[GearItem.SLOTS];
|
||||||
|
for (int s = 0; s < GearItem.SLOTS; s++)
|
||||||
|
{
|
||||||
|
ids[s] = worn.id(s);
|
||||||
|
}
|
||||||
|
if (lastWornIds == null || !java.util.Arrays.equals(ids, lastWornIds))
|
||||||
|
{
|
||||||
|
lastWornIds = ids;
|
||||||
|
gearChanged = true;
|
||||||
|
}
|
||||||
|
lastWorn = worn;
|
||||||
|
|
||||||
|
long hash = ownedHash();
|
||||||
|
if (hash != poolHash || gearChanged)
|
||||||
|
{
|
||||||
|
poolHash = hash;
|
||||||
|
pool = readPool(worn);
|
||||||
|
gearChanged = true;
|
||||||
|
}
|
||||||
|
containersChanged = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clearRequested)
|
||||||
|
{
|
||||||
|
clearRequested = false;
|
||||||
|
lastTarget = null;
|
||||||
|
targetNpcIndex = -1;
|
||||||
|
gearChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
FightSnapshot.Target target = readTarget(me);
|
||||||
|
Monster monster = target != null ? target.monster : null;
|
||||||
|
if (monster != lastMonster || monsterLoaded)
|
||||||
|
{
|
||||||
|
monsterLoaded = false;
|
||||||
|
lastMonster = monster;
|
||||||
|
gearChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gearChanged)
|
||||||
|
{
|
||||||
|
gearVersion++;
|
||||||
|
startSearch(gearVersion, worn, setup, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
FightSession s = combatTracker.getActiveSession();
|
||||||
|
FightSnapshot.Session session = s != null ? new FightSnapshot.Session(s) : null;
|
||||||
|
|
||||||
|
boolean emit = gearChanged || bestReady || target != null && (target.live || last.target == null || last.target.live)
|
||||||
|
|| session != null || last.session != null || target == null && last.target != null;
|
||||||
|
bestReady = false;
|
||||||
|
if (emit)
|
||||||
|
{
|
||||||
|
List<GearOptimizer.Option> b = bestVersion == gearVersion ? best : last.best;
|
||||||
|
publish(new FightSnapshot(true, setup, worn, pool, target, session, b, gearVersion));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void publish(FightSnapshot snap)
|
||||||
|
{
|
||||||
|
last = snap;
|
||||||
|
for (Consumer<FightSnapshot> l : listeners)
|
||||||
|
{
|
||||||
|
l.accept(snap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startSearch(long version, Loadout worn, PlayerSetup setup, FightSnapshot.Target target)
|
||||||
|
{
|
||||||
|
List<GearItem> items = pool;
|
||||||
|
Monster m = target != null ? target.calcMonster() : null;
|
||||||
|
if (m == null || items.isEmpty())
|
||||||
|
{
|
||||||
|
best = Collections.emptyList();
|
||||||
|
bestVersion = version;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
executor.execute(() ->
|
||||||
|
{
|
||||||
|
List<GearOptimizer.Option> result = GearOptimizer.bestPerStyle(items, worn, setup, m);
|
||||||
|
if (version == gearVersion)
|
||||||
|
{
|
||||||
|
best = result;
|
||||||
|
bestVersion = version;
|
||||||
|
bestReady = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- reading game state
|
||||||
|
|
||||||
|
private PlayerSetup readSetup()
|
||||||
|
{
|
||||||
|
double meleeAcc = pick(1.20, Prayer.PIETY, 1.15, Prayer.CHIVALRY, 1.15, Prayer.INCREDIBLE_REFLEXES,
|
||||||
|
1.10, Prayer.IMPROVED_REFLEXES, 1.05, Prayer.CLARITY_OF_THOUGHT);
|
||||||
|
double meleeStr = pick(1.23, Prayer.PIETY, 1.18, Prayer.CHIVALRY, 1.15, Prayer.ULTIMATE_STRENGTH,
|
||||||
|
1.10, Prayer.SUPERHUMAN_STRENGTH, 1.05, Prayer.BURST_OF_STRENGTH);
|
||||||
|
double rangedAcc = pick(1.20, Prayer.RIGOUR, 1.18, Prayer.DEADEYE, 1.15, Prayer.EAGLE_EYE,
|
||||||
|
1.10, Prayer.HAWK_EYE, 1.05, Prayer.SHARP_EYE);
|
||||||
|
double rangedStr = pick(1.23, Prayer.RIGOUR, 1.18, Prayer.DEADEYE, 1.15, Prayer.EAGLE_EYE,
|
||||||
|
1.10, Prayer.HAWK_EYE, 1.05, Prayer.SHARP_EYE);
|
||||||
|
double magicAcc = pick(1.25, Prayer.AUGURY, 1.18, Prayer.MYSTIC_VIGOUR, 1.15, Prayer.MYSTIC_MIGHT,
|
||||||
|
1.10, Prayer.MYSTIC_LORE, 1.05, Prayer.MYSTIC_WILL);
|
||||||
|
int magicDmg = client.isPrayerActive(Prayer.AUGURY) ? 4 : client.isPrayerActive(Prayer.MYSTIC_VIGOUR) ? 3
|
||||||
|
: client.isPrayerActive(Prayer.MYSTIC_MIGHT) ? 2 : client.isPrayerActive(Prayer.MYSTIC_LORE) ? 1 : 0;
|
||||||
|
return new PlayerSetup(
|
||||||
|
client.getBoostedSkillLevel(Skill.ATTACK),
|
||||||
|
client.getBoostedSkillLevel(Skill.STRENGTH),
|
||||||
|
client.getBoostedSkillLevel(Skill.DEFENCE),
|
||||||
|
client.getBoostedSkillLevel(Skill.RANGED),
|
||||||
|
client.getBoostedSkillLevel(Skill.MAGIC),
|
||||||
|
client.getBoostedSkillLevel(Skill.HITPOINTS),
|
||||||
|
meleeAcc, meleeStr, rangedAcc, rangedStr, magicAcc, magicDmg,
|
||||||
|
client.getVarbitValue(VarbitID.COMBAT_WEAPON_CATEGORY),
|
||||||
|
client.getVarpValue(VarPlayerID.COM_MODE),
|
||||||
|
client.getVarbitValue(VarbitID.AUTOCAST_SPELL),
|
||||||
|
slayerTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Multiplier of the first active prayer in (value, prayer) pairs, 1 if none. */
|
||||||
|
private double pick(Object... pairs)
|
||||||
|
{
|
||||||
|
for (int i = 0; i + 1 < pairs.length; i += 2)
|
||||||
|
{
|
||||||
|
if (client.isPrayerActive((Prayer) pairs[i + 1]))
|
||||||
|
{
|
||||||
|
return ((Number) pairs[i]).doubleValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean sameSetup(PlayerSetup a, PlayerSetup b)
|
||||||
|
{
|
||||||
|
return a.attack == b.attack && a.strength == b.strength && a.defence == b.defence && a.ranged == b.ranged
|
||||||
|
&& a.magic == b.magic && a.meleeAcc == b.meleeAcc && a.meleeStr == b.meleeStr && a.rangedAcc == b.rangedAcc
|
||||||
|
&& a.rangedStr == b.rangedStr && a.magicAcc == b.magicAcc && a.magicDmgPct == b.magicDmgPct
|
||||||
|
&& a.weaponCategory == b.weaponCategory && a.comMode == b.comMode && a.autocastSpell == b.autocastSpell
|
||||||
|
&& a.onSlayerTask == b.onSlayerTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Loadout readWorn()
|
||||||
|
{
|
||||||
|
GearItem[] items = new GearItem[GearItem.SLOTS];
|
||||||
|
ItemContainer c = client.getItemContainer(InventoryID.WORN);
|
||||||
|
if (c != null)
|
||||||
|
{
|
||||||
|
Item[] arr = c.getItems();
|
||||||
|
for (int s = 0; s < Math.min(arr.length, GearItem.SLOTS); s++)
|
||||||
|
{
|
||||||
|
if (arr[s] != null && arr[s].getId() > 0)
|
||||||
|
{
|
||||||
|
GearItem g = gearItem(arr[s].getId());
|
||||||
|
if (g == null)
|
||||||
|
{
|
||||||
|
// no bonuses known (e.g. new or cosmetic item): still show it
|
||||||
|
ItemComposition comp = itemManager.getItemComposition(arr[s].getId());
|
||||||
|
g = new GearItem(arr[s].getId(), comp.getName(), s, false, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, GearItem.Source.WORN);
|
||||||
|
}
|
||||||
|
items[s] = g;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Loadout(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long ownedHash()
|
||||||
|
{
|
||||||
|
long h = 17;
|
||||||
|
ItemContainer inv = client.getItemContainer(InventoryID.INV);
|
||||||
|
if (inv != null)
|
||||||
|
{
|
||||||
|
for (Item i : inv.getItems())
|
||||||
|
{
|
||||||
|
h = h * 31 + i.getId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<Integer, Long> b = bank.get();
|
||||||
|
if (b != null)
|
||||||
|
{
|
||||||
|
h = h * 31 + b.size();
|
||||||
|
for (Map.Entry<Integer, Long> e : b.entrySet())
|
||||||
|
{
|
||||||
|
h += e.getKey() * 1_000_003L ^ (e.getValue() > 0 ? 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<GearItem> readPool(Loadout worn)
|
||||||
|
{
|
||||||
|
Map<Integer, GearItem> out = new LinkedHashMap<>();
|
||||||
|
for (int s : GearItem.ALL_SLOTS)
|
||||||
|
{
|
||||||
|
GearItem g = worn.get(s);
|
||||||
|
if (g != null)
|
||||||
|
{
|
||||||
|
out.putIfAbsent(g.id, g.withSource(GearItem.Source.WORN));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ItemContainer inv = client.getItemContainer(InventoryID.INV);
|
||||||
|
if (inv != null)
|
||||||
|
{
|
||||||
|
for (Item i : inv.getItems())
|
||||||
|
{
|
||||||
|
addOwned(out, i.getId(), GearItem.Source.INVENTORY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<Integer, Long> b = bank.get();
|
||||||
|
if (b != null)
|
||||||
|
{
|
||||||
|
for (Map.Entry<Integer, Long> e : b.entrySet())
|
||||||
|
{
|
||||||
|
if (e.getValue() != null && e.getValue() > 0)
|
||||||
|
{
|
||||||
|
addOwned(out, e.getKey(), GearItem.Source.BANK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableList(new ArrayList<>(out.values()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addOwned(Map<Integer, GearItem> out, int id, GearItem.Source source)
|
||||||
|
{
|
||||||
|
if (id <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GearItem g = gearItem(id);
|
||||||
|
if (g != null && !out.containsKey(g.id))
|
||||||
|
{
|
||||||
|
out.put(g.id, g.withSource(source));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private GearItem gearItem(int rawId)
|
||||||
|
{
|
||||||
|
int id = itemManager.canonicalize(rawId);
|
||||||
|
if (itemCache.containsKey(id))
|
||||||
|
{
|
||||||
|
return itemCache.get(id);
|
||||||
|
}
|
||||||
|
GearItem g = null;
|
||||||
|
ItemStats stats = itemManager.getItemStats(id);
|
||||||
|
if (stats != null && stats.isEquipable() && stats.getEquipment() != null && stats.getEquipment().getSlot() >= 0
|
||||||
|
&& stats.getEquipment().getSlot() < GearItem.SLOTS)
|
||||||
|
{
|
||||||
|
ItemComposition comp = itemManager.getItemComposition(id);
|
||||||
|
g = GearItem.of(id, comp.getName(), stats.getEquipment(), GearItem.Source.WORN);
|
||||||
|
}
|
||||||
|
itemCache.put(id, g);
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FightSnapshot.Target readTarget(Player me)
|
||||||
|
{
|
||||||
|
NPC npc = null;
|
||||||
|
Actor interacting = me.getInteracting();
|
||||||
|
if (interacting instanceof NPC && ((NPC) interacting).getCombatLevel() > 0)
|
||||||
|
{
|
||||||
|
npc = (NPC) interacting;
|
||||||
|
}
|
||||||
|
FightSession session = combatTracker.getActiveSession();
|
||||||
|
if (npc == null && session != null)
|
||||||
|
{
|
||||||
|
npc = byIndex(session.getTargetIndex());
|
||||||
|
}
|
||||||
|
if (npc == null)
|
||||||
|
{
|
||||||
|
for (NPC n : client.getNpcs())
|
||||||
|
{
|
||||||
|
if (n != null && n.getInteracting() == me && n.getCombatLevel() > 0 && !n.isDead())
|
||||||
|
{
|
||||||
|
npc = n;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (npc == null)
|
||||||
|
{
|
||||||
|
if (lastTarget == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
NPC old = byIndex(targetNpcIndex);
|
||||||
|
boolean dead = lastTarget.dead || old == null || old.isDead();
|
||||||
|
if (lastTarget.live || lastTarget.monster == null)
|
||||||
|
{
|
||||||
|
Monster m = lastTarget.monster != null ? lastTarget.monster : monsters.get(lastTarget.npcId, lastTarget.combatLevel);
|
||||||
|
lastTarget = new FightSnapshot.Target(lastTarget.npcId, lastTarget.name, lastTarget.combatLevel,
|
||||||
|
dead ? 0 : lastTarget.healthRatio, lastTarget.healthScale, m, m == null && monsters.isLoading(lastTarget.npcId),
|
||||||
|
false, dead, null);
|
||||||
|
}
|
||||||
|
return lastTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetNpcIndex = npc.getIndex();
|
||||||
|
Monster m = monsters.get(npc.getId(), npc.getCombatLevel());
|
||||||
|
CombatStyle style = attackStyles.current().get(npc);
|
||||||
|
String name = Text.removeTags(npc.getName());
|
||||||
|
lastTarget = new FightSnapshot.Target(npc.getId(), name == null ? "Unknown" : name, npc.getCombatLevel(),
|
||||||
|
npc.getHealthScale() > 0 ? npc.getHealthRatio() : -1, npc.getHealthScale(), m,
|
||||||
|
m == null && monsters.isLoading(npc.getId()), true, npc.isDead(), style);
|
||||||
|
return lastTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
private NPC byIndex(int index)
|
||||||
|
{
|
||||||
|
if (index < 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (NPC n : client.getNpcs())
|
||||||
|
{
|
||||||
|
if (n != null && n.getIndex() == index)
|
||||||
|
{
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package com.example.essentialsplus.loot.gear;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.helpers.CombatStyle;
|
||||||
|
import com.example.essentialsplus.loot.combat.FightSession;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Everything the fight views show, captured on the client thread (immutable). */
|
||||||
|
public final class FightSnapshot
|
||||||
|
{
|
||||||
|
public static final FightSnapshot LOGGED_OUT = new FightSnapshot(false, PlayerSetup.DEFAULT, Loadout.EMPTY,
|
||||||
|
Collections.emptyList(), null, null, Collections.emptyList(), 0);
|
||||||
|
|
||||||
|
public static final class Target
|
||||||
|
{
|
||||||
|
public final int npcId;
|
||||||
|
public final String name;
|
||||||
|
public final int combatLevel;
|
||||||
|
/** -1 when no health bar is shown */
|
||||||
|
public final int healthRatio;
|
||||||
|
public final int healthScale;
|
||||||
|
/** null while loading or when the wiki has no stats */
|
||||||
|
public final Monster monster;
|
||||||
|
public final boolean loading;
|
||||||
|
/** being fought right now (false: last target, kept for planning) */
|
||||||
|
public final boolean live;
|
||||||
|
public final boolean dead;
|
||||||
|
/** attack style the NPC last used on the player, null if unknown */
|
||||||
|
public final CombatStyle attackStyle;
|
||||||
|
|
||||||
|
public Target(int npcId, String name, int combatLevel, int healthRatio, int healthScale, Monster monster,
|
||||||
|
boolean loading, boolean live, boolean dead, CombatStyle attackStyle)
|
||||||
|
{
|
||||||
|
this.npcId = npcId;
|
||||||
|
this.name = name;
|
||||||
|
this.combatLevel = combatLevel;
|
||||||
|
this.healthRatio = healthRatio;
|
||||||
|
this.healthScale = healthScale;
|
||||||
|
this.monster = monster;
|
||||||
|
this.loading = loading;
|
||||||
|
this.live = live;
|
||||||
|
this.dead = dead;
|
||||||
|
this.attackStyle = attackStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Monster to calculate against: wiki stats or a dummy. */
|
||||||
|
public Monster calcMonster()
|
||||||
|
{
|
||||||
|
return monster != null ? monster : Monster.DUMMY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Copy of the running fight session. */
|
||||||
|
public static final class Session
|
||||||
|
{
|
||||||
|
public final int durationTicks, idleTicks, attacks, damageDealt, hitsReceived, damageTaken, hpHealed;
|
||||||
|
public final int prayerBlocked, prayerMissed, onPrayerAttacks, offPrayerAttacks, correctWeapon, incorrectWeapon;
|
||||||
|
public final long suppliesCost;
|
||||||
|
public final double efficiency, performance, prayerDefencePct, offPrayerPct, correctWeaponPct;
|
||||||
|
public final String grade;
|
||||||
|
|
||||||
|
public Session(FightSession s)
|
||||||
|
{
|
||||||
|
durationTicks = s.getDurationTicks();
|
||||||
|
idleTicks = s.getIdleTicks();
|
||||||
|
attacks = s.getAttacksDealt();
|
||||||
|
damageDealt = s.getDamageDealt();
|
||||||
|
hitsReceived = s.getHitsReceived();
|
||||||
|
damageTaken = s.getDamageTaken();
|
||||||
|
hpHealed = s.getHpHealed();
|
||||||
|
prayerBlocked = s.getPrayerBlockedHits();
|
||||||
|
prayerMissed = s.getPrayerMissedHits();
|
||||||
|
onPrayerAttacks = s.getOnPrayerAttacks();
|
||||||
|
offPrayerAttacks = s.getOffPrayerAttacks();
|
||||||
|
correctWeapon = s.getCorrectWeaponAttacks();
|
||||||
|
incorrectWeapon = s.getIncorrectWeaponAttacks();
|
||||||
|
suppliesCost = s.getSuppliesCost();
|
||||||
|
efficiency = s.getEfficiencyPct();
|
||||||
|
performance = s.getPerformanceScore();
|
||||||
|
prayerDefencePct = s.getPrayerDefensePct();
|
||||||
|
offPrayerPct = s.getOffPrayerAttackPct();
|
||||||
|
correctWeaponPct = s.getCorrectWeaponPct();
|
||||||
|
grade = s.getGrade();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final boolean loggedIn;
|
||||||
|
public final PlayerSetup setup;
|
||||||
|
public final Loadout worn;
|
||||||
|
/** Equipable items owned (worn, inventory, bank), one per item id */
|
||||||
|
public final List<GearItem> pool;
|
||||||
|
public final Target target;
|
||||||
|
public final Session session;
|
||||||
|
/** Best setup per style against the target, highest DPS first */
|
||||||
|
public final List<GearOptimizer.Option> best;
|
||||||
|
/** changes whenever gear, pool, levels or target stats change (not on hp / session updates) */
|
||||||
|
public final long gearVersion;
|
||||||
|
|
||||||
|
public FightSnapshot(boolean loggedIn, PlayerSetup setup, Loadout worn, List<GearItem> pool, Target target,
|
||||||
|
Session session, List<GearOptimizer.Option> best, long gearVersion)
|
||||||
|
{
|
||||||
|
this.loggedIn = loggedIn;
|
||||||
|
this.setup = setup;
|
||||||
|
this.worn = worn;
|
||||||
|
this.pool = pool;
|
||||||
|
this.target = target;
|
||||||
|
this.session = session;
|
||||||
|
this.best = best;
|
||||||
|
this.gearVersion = gearVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package com.example.essentialsplus.loot.gear;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import net.runelite.client.game.ItemEquipmentStats;
|
||||||
|
|
||||||
|
/** An equipable item with its bonuses (immutable, safe on any thread). */
|
||||||
|
public final class GearItem
|
||||||
|
{
|
||||||
|
public static final int HEAD = 0;
|
||||||
|
public static final int CAPE = 1;
|
||||||
|
public static final int AMULET = 2;
|
||||||
|
public static final int WEAPON = 3;
|
||||||
|
public static final int BODY = 4;
|
||||||
|
public static final int SHIELD = 5;
|
||||||
|
public static final int LEGS = 7;
|
||||||
|
public static final int GLOVES = 9;
|
||||||
|
public static final int BOOTS = 10;
|
||||||
|
public static final int RING = 12;
|
||||||
|
public static final int AMMO = 13;
|
||||||
|
public static final int SLOTS = 14;
|
||||||
|
|
||||||
|
public static final int[] ALL_SLOTS = {HEAD, CAPE, AMULET, WEAPON, BODY, SHIELD, LEGS, GLOVES, BOOTS, RING, AMMO};
|
||||||
|
|
||||||
|
/** Where the item was found. */
|
||||||
|
public enum Source
|
||||||
|
{
|
||||||
|
WORN("Worn"),
|
||||||
|
INVENTORY("Inventory"),
|
||||||
|
BANK("Bank");
|
||||||
|
|
||||||
|
public final String label;
|
||||||
|
|
||||||
|
Source(String label)
|
||||||
|
{
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final int id;
|
||||||
|
public final String name;
|
||||||
|
public final String lower;
|
||||||
|
public final int slot;
|
||||||
|
public final boolean twoHanded;
|
||||||
|
public final int astab, aslash, acrush, amagic, arange;
|
||||||
|
public final int dstab, dslash, dcrush, dmagic, drange;
|
||||||
|
public final int str, rstr, prayer, aspeed;
|
||||||
|
/** magic damage in percent */
|
||||||
|
public final double mdmg;
|
||||||
|
public final Source source;
|
||||||
|
|
||||||
|
public GearItem(int id, String name, int slot, boolean twoHanded,
|
||||||
|
int astab, int aslash, int acrush, int amagic, int arange,
|
||||||
|
int dstab, int dslash, int dcrush, int dmagic, int drange,
|
||||||
|
int str, int rstr, double mdmg, int prayer, int aspeed, Source source)
|
||||||
|
{
|
||||||
|
this.id = id;
|
||||||
|
this.name = name == null ? "Item " + id : name;
|
||||||
|
this.lower = this.name.toLowerCase(Locale.ENGLISH);
|
||||||
|
this.slot = slot;
|
||||||
|
this.twoHanded = twoHanded;
|
||||||
|
this.astab = astab;
|
||||||
|
this.aslash = aslash;
|
||||||
|
this.acrush = acrush;
|
||||||
|
this.amagic = amagic;
|
||||||
|
this.arange = arange;
|
||||||
|
this.dstab = dstab;
|
||||||
|
this.dslash = dslash;
|
||||||
|
this.dcrush = dcrush;
|
||||||
|
this.dmagic = dmagic;
|
||||||
|
this.drange = drange;
|
||||||
|
this.str = str;
|
||||||
|
this.rstr = rstr;
|
||||||
|
this.mdmg = mdmg;
|
||||||
|
this.prayer = prayer;
|
||||||
|
this.aspeed = aspeed;
|
||||||
|
this.source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GearItem of(int id, String name, ItemEquipmentStats e, Source source)
|
||||||
|
{
|
||||||
|
return new GearItem(id, name, e.getSlot(), e.isTwoHanded(),
|
||||||
|
e.getAstab(), e.getAslash(), e.getAcrush(), e.getAmagic(), e.getArange(),
|
||||||
|
e.getDstab(), e.getDslash(), e.getDcrush(), e.getDmagic(), e.getDrange(),
|
||||||
|
e.getStr(), e.getRstr(), e.getMdmg(), e.getPrayer(), e.getAspeed(), source);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GearItem withSource(Source s)
|
||||||
|
{
|
||||||
|
return s == source ? this : new GearItem(id, name, slot, twoHanded, astab, aslash, acrush, amagic, arange,
|
||||||
|
dstab, dslash, dcrush, dmagic, drange, str, rstr, mdmg, prayer, aspeed, s);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int totalDefence()
|
||||||
|
{
|
||||||
|
return dstab + dslash + dcrush + dmagic + drange;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean has(String part)
|
||||||
|
{
|
||||||
|
return lower.contains(part);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String slotName(int slot)
|
||||||
|
{
|
||||||
|
switch (slot)
|
||||||
|
{
|
||||||
|
case HEAD: return "Head";
|
||||||
|
case CAPE: return "Cape";
|
||||||
|
case AMULET: return "Neck";
|
||||||
|
case WEAPON: return "Weapon";
|
||||||
|
case BODY: return "Body";
|
||||||
|
case SHIELD: return "Shield";
|
||||||
|
case LEGS: return "Legs";
|
||||||
|
case GLOVES: return "Hands";
|
||||||
|
case BOOTS: return "Feet";
|
||||||
|
case RING: return "Ring";
|
||||||
|
case AMMO: return "Ammo";
|
||||||
|
default: return "Slot " + slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
package com.example.essentialsplus.loot.gear;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.loot.gear.CombatChoice.Type;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Picks the highest DPS items per combat style from the items the player owns. */
|
||||||
|
public final class GearOptimizer
|
||||||
|
{
|
||||||
|
private static final int WEAPONS_PER_STYLE = 8;
|
||||||
|
private static final int PASSES = 2;
|
||||||
|
|
||||||
|
private GearOptimizer()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Option
|
||||||
|
{
|
||||||
|
public final String label;
|
||||||
|
public final Loadout loadout;
|
||||||
|
public final DpsCalc.Result result;
|
||||||
|
|
||||||
|
Option(String label, Loadout loadout, DpsCalc.Result result)
|
||||||
|
{
|
||||||
|
this.label = label;
|
||||||
|
this.loadout = loadout;
|
||||||
|
this.result = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Best setup of each style (melee, ranged, magic) that has a weapon, highest DPS first. */
|
||||||
|
public static List<Option> bestPerStyle(Collection<GearItem> pool, Loadout worn, PlayerSetup p, Monster m)
|
||||||
|
{
|
||||||
|
List<Option> out = new ArrayList<>();
|
||||||
|
for (Type cls : new Type[]{Type.STAB, Type.RANGED, Type.MAGIC})
|
||||||
|
{
|
||||||
|
Option best = bestFor(cls, pool, worn, p, m);
|
||||||
|
if (best != null && best.result.dps > 0)
|
||||||
|
{
|
||||||
|
out.add(best);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.sort(Comparator.comparingDouble((Option o) -> o.result.dps).reversed());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<CombatChoice> choices(Type cls, GearItem w, PlayerSetup p)
|
||||||
|
{
|
||||||
|
List<CombatChoice> out = new ArrayList<>();
|
||||||
|
if (cls == Type.STAB)
|
||||||
|
{
|
||||||
|
if (w.astab > 0)
|
||||||
|
{
|
||||||
|
out.add(CombatChoice.melee(Type.STAB));
|
||||||
|
}
|
||||||
|
if (w.aslash > 0)
|
||||||
|
{
|
||||||
|
out.add(CombatChoice.melee(Type.SLASH));
|
||||||
|
}
|
||||||
|
if (w.acrush > 0)
|
||||||
|
{
|
||||||
|
out.add(CombatChoice.melee(Type.CRUSH));
|
||||||
|
}
|
||||||
|
if (out.isEmpty())
|
||||||
|
{
|
||||||
|
out.add(CombatChoice.melee(CombatChoice.bestMeleeType(w)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
out.add(CombatChoice.infer(w, p.magic, p.autocastSpell));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Option bestFor(Type cls, Collection<GearItem> pool, Loadout worn, PlayerSetup p, Monster m)
|
||||||
|
{
|
||||||
|
// rank weapons on their own first so the full search stays small
|
||||||
|
List<Option> starts = new ArrayList<>();
|
||||||
|
for (GearItem w : pool)
|
||||||
|
{
|
||||||
|
if (w.slot != GearItem.WEAPON || CombatChoice.weaponClass(w) != cls)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Loadout l = worn.with(GearItem.WEAPON, w);
|
||||||
|
for (CombatChoice c : choices(cls, w, p))
|
||||||
|
{
|
||||||
|
starts.add(new Option(label(c), l, DpsCalc.calc(l, p, m, c)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
starts.sort(Comparator.comparingDouble((Option o) -> o.result.dps).reversed());
|
||||||
|
|
||||||
|
Option best = null;
|
||||||
|
for (int i = 0; i < Math.min(WEAPONS_PER_STYLE, starts.size()); i++)
|
||||||
|
{
|
||||||
|
Option o = improve(starts.get(i), pool, p, m);
|
||||||
|
if (best == null || o.result.dps > best.result.dps + 1e-9)
|
||||||
|
{
|
||||||
|
best = o;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Option improve(Option start, Collection<GearItem> pool, PlayerSetup p, Monster m)
|
||||||
|
{
|
||||||
|
CombatChoice c = start.result.choice;
|
||||||
|
Loadout l = start.loadout;
|
||||||
|
DpsCalc.Result r = start.result;
|
||||||
|
for (int pass = 0; pass < PASSES; pass++)
|
||||||
|
{
|
||||||
|
boolean changed = false;
|
||||||
|
for (int slot : GearItem.ALL_SLOTS)
|
||||||
|
{
|
||||||
|
if (slot == GearItem.WEAPON || slot == GearItem.SHIELD && l.weapon() != null && l.weapon().twoHanded)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (GearItem g : pool)
|
||||||
|
{
|
||||||
|
if (g.slot != slot || g.id == l.id(slot))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Loadout candidate = l.with(slot, g);
|
||||||
|
DpsCalc.Result cr = DpsCalc.calc(candidate, p, m, c);
|
||||||
|
if (cr.dps > r.dps + 1e-9)
|
||||||
|
{
|
||||||
|
l = candidate;
|
||||||
|
r = cr;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!changed)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Option(start.label, l, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String label(CombatChoice c)
|
||||||
|
{
|
||||||
|
switch (c.type)
|
||||||
|
{
|
||||||
|
case RANGED:
|
||||||
|
return "Ranged";
|
||||||
|
case MAGIC:
|
||||||
|
return "Magic";
|
||||||
|
default:
|
||||||
|
return "Melee (" + c.type.label.toLowerCase() + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user