Files
Runelite-EssentialsPlus/skill-reset-hourly.md
T
2026-09-18 17:18:57 +02:00

95 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.