Files
Runelite-EssentialsPlus/skill-reset-hourly.md
T

95 lines
7.4 KiB
Markdown
Raw Normal View History

2026-09-18 17:18:53 +02:00
# 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.