From c61ab9c9519b516bc624487ced402d240278f5b8 Mon Sep 17 00:00:00 2001 From: Reasel Date: Mon, 22 Jun 2026 15:14:35 -0700 Subject: [PATCH 1/4] docs: design for avg click distance metric --- .../2026-06-22-avg-click-distance-design.md | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 docs/plans/2026-06-22-avg-click-distance-design.md diff --git a/docs/plans/2026-06-22-avg-click-distance-design.md b/docs/plans/2026-06-22-avg-click-distance-design.md new file mode 100644 index 0000000..a5987af --- /dev/null +++ b/docs/plans/2026-06-22-avg-click-distance-design.md @@ -0,0 +1,86 @@ +# Avg Click Distance Metric — Design + +## Goal + +Add a third live metric: **Average Click Distance** — the mean Euclidean distance (in pixels) the mouse jumps between consecutive clicks. Parallel to the existing Avg Click Interval metric (which measures *time* between clicks), this measures *distance* between clicks. Lets a user discuss how much they had to move their mouse during an AFK session. + +## Definition + +For clicks recorded in time order with positions `p_1 … p_n`: + +``` +avgDistance = ( Σ distance(p_i, p_{i-1}) for i in 2..n ) / (n - 1) +``` + +- `distance` = Euclidean: `sqrt(dx² + dy²)`. +- Points are in click order (time order), **not** sorted — distance is between consecutive clicks as they happened. +- Fewer than 2 clicks → `0.0` (no pair to measure), mirroring `getAverageClickInterval`. +- Coordinates come from `MouseEvent.getPoint()`, relative to the RuneLite game canvas. Pixel distances therefore vary with canvas size / display mode; this is inherent and not corrected for. + +## Components + +### 1. Data capture — `MouseClickCounterListener` + +Add a parallel `List clickPoints` alongside the existing `List clickTimestamps`. + +- `mousePressed` records `mouseEvent.getPoint()` at the same call site that records the timestamp. +- New synchronized `getClickPoints()` returning a defensive copy. +- `resetMouseClickCounterListener()` clears both lists. + +Rationale: a parallel list keeps the existing `List` timestamp API and the `computeConsistency` / `getAverageClickInterval` signatures untouched. No `Click(ts,x,y)` record type — that would churn the existing methods for no gain. + +### 2. Calculation — `AfkStatsTrackerPlugin` + +New `public double getAverageClickDistance()`, mirroring `getAverageClickInterval`: + +``` +List points = mouseListener.getClickPoints(); +int n = points.size(); +if (n < 2) return 0.0; +double sum = 0.0; +for (int i = 1; i < n; i++) sum += points.get(i).distance(points.get(i - 1)); +return sum / (n - 1); +``` + +Uses `java.awt.Point.distance(Point)` for the Euclidean calc. No sorting (points already in click order). + +### 3. Model — `Session` + +Add `private final double avgDistance;` with a getter, as the last constructor argument. `stopSession()` passes `getAverageClickDistance()`. + +Backward compatibility: sessions persisted before this change have no `avgDistance` in their JSON; gson deserializes the field to `0.0`. These render as `0 px`. Acceptable — no migration needed. + +### 4. UI — `AfkStatsTrackerPanel` + +- New **"Avg Click Distance"** stat card (clone of the Avg Click Interval card), value formatted `"%.0f px"`. Tooltip: "Average distance the mouse moved between clicks, in pixels." +- New inner class **`DistanceIndicator`** — a clone of `IntervalIndicator` using a **linear** scale (not logarithmic): + - Range `0–1000 px`. Ticks at `0 / 250 / 500 / 750 / 1000`, labels at each. + - Linear because click distances are bounded by canvas size — no orders-of-magnitude spread that would justify a log scale. + - The `1000 px` ceiling is an initial guess; tune if real-world averages cluster at the low end. +- Place the new card + indicator after the interval card/indicator in `statsPanel`. +- `updateStats()` reads `plugin.getAverageClickDistance()`, updates the card label and `distanceIndicator.setValue(...)`. +- History row stats line (`createSessionRow`): extend the format to include distance, e.g. `"%d | %.0fms | %.0fpx | %d clicks"`. +- Copy-to-clipboard text: add an `Avg Distance: %.0fpx` line. + +## Testing + +Extend the existing test suite (mirroring the interval tests): + +- 0 clicks → `0.0` +- 1 click → `0.0` +- Known points, e.g. `(0,0) → (3,4) → (3,4)` → distances `5, 0` → avg `2.5` +- A horizontal/vertical case with an exact expected average + +## Scope + +Files touched: `MouseClickCounterListener`, `AfkStatsTrackerPlugin`, `Session`, `AfkStatsTrackerPanel`, plus the test class. One new inner class (`DistanceIndicator`). + +## Out of scope (YAGNI) + +- Tracking the *full* mouse path (every `mouseMoved`/`mouseDragged`) — heavier, not what was asked. +- A consistency-style score for distance — only the average was requested. +- Configurable indicator scale — hardcode `1000 px`, revisit only if needed. + +## Follow-up + +Panel UI changes → after implementation, capture an updated panel screenshot, refresh `panel-preview.png` + README reference, and include it in the PR description. From 26c73c7a27673ca76f57c2738dbeefdc6ca78476 Mon Sep 17 00:00:00 2001 From: Reasel Date: Mon, 22 Jun 2026 15:25:02 -0700 Subject: [PATCH 2/4] docs: normalize avg click distance to % of canvas diagonal --- .../2026-06-22-avg-click-distance-design.md | 92 +++++++++++++------ 1 file changed, 63 insertions(+), 29 deletions(-) diff --git a/docs/plans/2026-06-22-avg-click-distance-design.md b/docs/plans/2026-06-22-avg-click-distance-design.md index a5987af..454dfc4 100644 --- a/docs/plans/2026-06-22-avg-click-distance-design.md +++ b/docs/plans/2026-06-22-avg-click-distance-design.md @@ -2,20 +2,24 @@ ## Goal -Add a third live metric: **Average Click Distance** — the mean Euclidean distance (in pixels) the mouse jumps between consecutive clicks. Parallel to the existing Avg Click Interval metric (which measures *time* between clicks), this measures *distance* between clicks. Lets a user discuss how much they had to move their mouse during an AFK session. +Add a third live metric: **Average Click Distance** — the mean distance the mouse jumps between consecutive clicks, expressed as a **percentage of the game canvas diagonal**. Parallel to the existing Avg Click Interval metric (which measures *time* between clicks), this measures *distance* between clicks. Lets a user discuss how much they had to move their mouse during an AFK session, independent of resolution / display mode. ## Definition -For clicks recorded in time order with positions `p_1 … p_n`: +For clicks recorded in time order with positions `p_1 … p_n` (pixels), and a canvas of width `w`, height `h`: ``` -avgDistance = ( Σ distance(p_i, p_{i-1}) for i in 2..n ) / (n - 1) +avgPixelDist = ( Σ distance(p_i, p_{i-1}) for i in 2..n ) / (n - 1) +diagonal = sqrt(w² + h²) +avgDistancePercent = avgPixelDist / diagonal × 100 ``` - `distance` = Euclidean: `sqrt(dx² + dy²)`. +- Normalized against the canvas **diagonal** — the maximum possible distance between two on-canvas points — so the value is a clean `0–100%` regardless of resolution or fixed/resizable mode. - Points are in click order (time order), **not** sorted — distance is between consecutive clicks as they happened. -- Fewer than 2 clicks → `0.0` (no pair to measure), mirroring `getAverageClickInterval`. -- Coordinates come from `MouseEvent.getPoint()`, relative to the RuneLite game canvas. Pixel distances therefore vary with canvas size / display mode; this is inherent and not corrected for. +- Fewer than 2 clicks → `0.0`. Also `0.0` if `diagonal <= 0` (canvas not yet available, e.g. before login). Mirrors `getAverageClickInterval`'s `< 2` guard. +- Coordinates come from `MouseEvent.getPoint()`, relative to the game canvas. Canvas size comes from `client.getCanvasWidth()` / `client.getCanvasHeight()` (confirmed present in the RuneLite API). +- Caveat: if the canvas is resized mid-session, stored pixel distances are normalized by the canvas diagonal at compute time, introducing a small skew. Acceptable for an AFK stat; not corrected for. ## Components @@ -31,45 +35,75 @@ Rationale: a parallel list keeps the existing `List` timestamp API and the ### 2. Calculation — `AfkStatsTrackerPlugin` -New `public double getAverageClickDistance()`, mirroring `getAverageClickInterval`: +Split into a pure, testable helper plus a thin live wrapper — mirrors how `computeConsistency(List)` is separated from `getConsistency()` so the math is unit-testable without a live client. + +Pure helper (no client dependency): ``` -List points = mouseListener.getClickPoints(); -int n = points.size(); -if (n < 2) return 0.0; -double sum = 0.0; -for (int i = 1; i < n; i++) sum += points.get(i).distance(points.get(i - 1)); -return sum / (n - 1); +// Average Euclidean pixel distance between consecutive clicks. < 2 points → 0. +public double computeAverageDistance(List points) +{ + int n = points.size(); + if (n < 2) return 0.0; + double sum = 0.0; + for (int i = 1; i < n; i++) sum += points.get(i).distance(points.get(i - 1)); + return sum / (n - 1); +} + +// Normalize an average pixel distance to % of the canvas diagonal. diagonal <= 0 → 0. +public double toDistancePercent(double avgPixelDist, int canvasWidth, int canvasHeight) +{ + double diagonal = Math.hypot(canvasWidth, canvasHeight); + if (diagonal <= 0) return 0.0; + return avgPixelDist / diagonal * 100.0; +} ``` -Uses `java.awt.Point.distance(Point)` for the Euclidean calc. No sorting (points already in click order). +Live wrapper: + +``` +public double getAverageClickDistance() +{ + double avgPx = computeAverageDistance(mouseListener.getClickPoints()); + return toDistancePercent(avgPx, client.getCanvasWidth(), client.getCanvasHeight()); +} +``` + +Uses `java.awt.Point.distance(Point)` for the Euclidean calc and `Math.hypot` for the diagonal. No sorting (points already in click order). ### 3. Model — `Session` -Add `private final double avgDistance;` with a getter, as the last constructor argument. `stopSession()` passes `getAverageClickDistance()`. +Add `private final double avgDistancePercent;` with a getter, as the last constructor argument. `stopSession()` passes `getAverageClickDistance()`. -Backward compatibility: sessions persisted before this change have no `avgDistance` in their JSON; gson deserializes the field to `0.0`. These render as `0 px`. Acceptable — no migration needed. +Backward compatibility: sessions persisted before this change have no `avgDistancePercent` in their JSON; gson deserializes the field to `0.0`. These render as `0.0%`. Acceptable — no migration needed. ### 4. UI — `AfkStatsTrackerPanel` -- New **"Avg Click Distance"** stat card (clone of the Avg Click Interval card), value formatted `"%.0f px"`. Tooltip: "Average distance the mouse moved between clicks, in pixels." -- New inner class **`DistanceIndicator`** — a clone of `IntervalIndicator` using a **linear** scale (not logarithmic): - - Range `0–1000 px`. Ticks at `0 / 250 / 500 / 750 / 1000`, labels at each. - - Linear because click distances are bounded by canvas size — no orders-of-magnitude spread that would justify a log scale. - - The `1000 px` ceiling is an initial guess; tune if real-world averages cluster at the low end. +- New **"Avg Click Distance"** stat card (clone of the Avg Click Interval card), value formatted `"%.1f%%"`. Tooltip: "Average distance the mouse moved between clicks, as a percentage of the canvas diagonal." +- New inner class **`DistanceIndicator`** — a linear `0–100` indicator. Model it on `ConsistencyIndicator` (already a linear 0–100 slider) rather than the logarithmic `IntervalIndicator`, but take a `double` value: + - Range `0–100%`. Ticks every `25` (`0 / 25 / 50 / 75 / 100`), labels at `0` and `100` with a `%` suffix. + - `setValue(double)` clamps to `[0, 100]`. - Place the new card + indicator after the interval card/indicator in `statsPanel`. -- `updateStats()` reads `plugin.getAverageClickDistance()`, updates the card label and `distanceIndicator.setValue(...)`. -- History row stats line (`createSessionRow`): extend the format to include distance, e.g. `"%d | %.0fms | %.0fpx | %d clicks"`. -- Copy-to-clipboard text: add an `Avg Distance: %.0fpx` line. +- `updateStats()` reads `plugin.getAverageClickDistance()`, updates the card label (`"%.1f%%"`) and `distanceIndicator.setValue(...)`. +- History row stats line (`createSessionRow`): extend the format to include distance, e.g. `"%d | %.0fms | %.1f%% | %d clicks"`. +- Copy-to-clipboard text: add an `Avg Distance: %.1f%%` line. ## Testing -Extend the existing test suite (mirroring the interval tests): +Extend the existing test suite, targeting the two pure helpers (no client mock needed): + +`computeAverageDistance(List)`: +- 0 points → `0.0` +- 1 point → `0.0` +- `(0,0) → (3,4) → (3,4)` → distances `5, 0` → avg `2.5` +- `(0,0) → (0,10) → (10,10)` → distances `10, 10` → avg `10.0` + +`toDistancePercent(avgPixelDist, w, h)`: +- `w = h = 0` (or either ≤ 0) → `0.0` +- `avgPixelDist = 50`, canvas `300×400` (diagonal `500`) → `10.0` +- `avgPixelDist = 500`, canvas `300×400` → `100.0` (full-diagonal move) -- 0 clicks → `0.0` -- 1 click → `0.0` -- Known points, e.g. `(0,0) → (3,4) → (3,4)` → distances `5, 0` → avg `2.5` -- A horizontal/vertical case with an exact expected average +Use a small delta for double assertions (e.g. `assertEquals(expected, actual, 1e-9)`). ## Scope @@ -79,7 +113,7 @@ Files touched: `MouseClickCounterListener`, `AfkStatsTrackerPlugin`, `Session`, - Tracking the *full* mouse path (every `mouseMoved`/`mouseDragged`) — heavier, not what was asked. - A consistency-style score for distance — only the average was requested. -- Configurable indicator scale — hardcode `1000 px`, revisit only if needed. +- Correcting for mid-session canvas resize — normalize at compute time, accept the small skew. ## Follow-up From 98dad02d8418dcd6a9ae498fb18b61c3b60f7180 Mon Sep 17 00:00:00 2001 From: Reasel Date: Mon, 22 Jun 2026 15:29:50 -0700 Subject: [PATCH 3/4] docs: implementation plan for avg click distance metric --- docs/plans/2026-06-22-avg-click-distance.md | 661 ++++++++++++++++++++ 1 file changed, 661 insertions(+) create mode 100644 docs/plans/2026-06-22-avg-click-distance.md diff --git a/docs/plans/2026-06-22-avg-click-distance.md b/docs/plans/2026-06-22-avg-click-distance.md new file mode 100644 index 0000000..b9b103f --- /dev/null +++ b/docs/plans/2026-06-22-avg-click-distance.md @@ -0,0 +1,661 @@ +# Avg Click Distance Metric Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a live "Avg Click Distance" metric — mean mouse-jump between consecutive clicks, shown as a percentage of the game canvas diagonal — alongside the existing consistency and interval metrics. + +**Architecture:** The mouse listener records each click's `(x,y)` point in parallel with its timestamp. The plugin computes the average Euclidean pixel distance between consecutive clicks (pure helper), then normalizes it against the canvas diagonal to a 0–100% value (second pure helper). The `Session` model gains an `avgDistancePercent` field; the panel gains a stat card and a linear 0–100 `DistanceIndicator`. + +**Tech Stack:** Java 11, RuneLite plugin API (`net.runelite.api.Client`, `MouseManager`), Swing panel UI, JUnit 4, Lombok, Gson. Build via `nix-shell -p jdk11 --run "./gradlew ..."`. + +## Global Constraints + +- Java 11 target. Build/test commands MUST be wrapped: `nix-shell -p jdk11 --run "./gradlew "`. +- The new metric is a **percentage of the canvas diagonal**, range `0–100`, displayed as `"%.1f%%"`. +- Distance points are kept in **click (time) order — never sorted**. +- Fewer than 2 clicks, or canvas diagonal `<= 0`, → `0.0`. +- Follow existing code style: no Lombok on the touched classes beyond what's already there, 4-space indentation matching each file. + +--- + +### Task 1: Capture click coordinates in the listener + +**Files:** +- Modify: `src/main/java/com/afkstatstracker/MouseClickCounterListener.java` +- Test: `src/test/java/com/afkstatstracker/MouseClickCounterListenerTest.java` + +**Interfaces:** +- Consumes: nothing new. +- Produces: + - `void addClick(int x, int y)` — records a timestamp **and** a `Point(x,y)`. + - `void addClick()` — unchanged signature; now delegates to `addClick(0, 0)` so the timestamp and point lists stay equal length. + - `List getClickPoints()` — defensive copy, in click order. + - `mousePressed` records `mouseEvent.getX()`, `mouseEvent.getY()`. + - `resetMouseClickCounterListener()` clears points too. + +- [ ] **Step 1: Write the failing tests** + +Add to `MouseClickCounterListenerTest.java` (add imports `java.awt.Point` and keep existing ones): + +```java + @Test + public void testAddClickWithCoordsRecordsPoint() + { + MouseClickCounterListener listener = new MouseClickCounterListener(); + listener.addClick(10, 20); + listener.addClick(30, 40); + List points = listener.getClickPoints(); + assertEquals(2, points.size()); + assertEquals(new Point(10, 20), points.get(0)); + assertEquals(new Point(30, 40), points.get(1)); + } + + @Test + public void testNoArgAddClickKeepsListsParallel() + { + MouseClickCounterListener listener = new MouseClickCounterListener(); + listener.addClick(); + assertEquals(1, listener.getClickCounter().size()); + assertEquals(1, listener.getClickPoints().size()); + } + + @Test + public void testResetClearsPoints() + { + MouseClickCounterListener listener = new MouseClickCounterListener(); + listener.addClick(5, 5); + listener.resetMouseClickCounterListener(); + assertTrue(listener.getClickPoints().isEmpty()); + } + + @Test + public void testGetClickPointsReturnsDefensiveCopy() + { + MouseClickCounterListener listener = new MouseClickCounterListener(); + listener.addClick(1, 1); + List copy = listener.getClickPoints(); + copy.clear(); + assertEquals(1, listener.getClickPoints().size()); + } +``` + +Add the import at the top of the test file: + +```java +import java.awt.Point; +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `nix-shell -p jdk11 --run "./gradlew test --tests com.afkstatstracker.MouseClickCounterListenerTest"` +Expected: FAIL — compile error / `cannot find symbol: method getClickPoints()` and `addClick(int,int)`. + +- [ ] **Step 3: Implement the change** + +Replace the full contents of `MouseClickCounterListener.java` with: + +```java +package com.afkstatstracker; + +import java.awt.Point; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.List; +import net.runelite.client.input.MouseAdapter; + +public class MouseClickCounterListener extends MouseAdapter +{ + private final List clickTimestamps = new ArrayList<>(); + private final List clickPoints = new ArrayList<>(); + + @Override + public MouseEvent mousePressed(MouseEvent mouseEvent) + { + addClick(mouseEvent.getX(), mouseEvent.getY()); + return mouseEvent; + } + + public synchronized List getClickCounter() + { + return new ArrayList<>(clickTimestamps); + } + + public synchronized List getClickPoints() + { + return new ArrayList<>(clickPoints); + } + + public synchronized void addClick() + { + addClick(0, 0); + } + + public synchronized void addClick(int x, int y) + { + clickTimestamps.add(System.currentTimeMillis()); + clickPoints.add(new Point(x, y)); + } + + public synchronized void resetMouseClickCounterListener() + { + this.clickTimestamps.clear(); + this.clickPoints.clear(); + } +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `nix-shell -p jdk11 --run "./gradlew test --tests com.afkstatstracker.MouseClickCounterListenerTest"` +Expected: PASS (all listener tests, old and new). + +- [ ] **Step 5: Commit** + +```bash +git add src/main/java/com/afkstatstracker/MouseClickCounterListener.java src/test/java/com/afkstatstracker/MouseClickCounterListenerTest.java +git commit -m "feat: record click coordinates in mouse listener" +``` + +--- + +### Task 2: Distance calculation helpers in the plugin + +**Files:** +- Modify: `src/main/java/com/afkstatstracker/AfkStatsTrackerPlugin.java` +- Test: `src/test/java/com/afkstatstracker/AfkStatsTrackerCalcTest.java` (create) + +**Interfaces:** +- Consumes: `MouseClickCounterListener.getClickPoints()` (Task 1); `client.getCanvasWidth()`, `client.getCanvasHeight()`. +- Produces: + - `double computeAverageDistance(List points)` — avg Euclidean pixel distance between consecutive points; `< 2` → `0.0`. + - `double toDistancePercent(double avgPixelDist, int canvasWidth, int canvasHeight)` — `avgPixelDist / hypot(w,h) * 100`; diagonal `<= 0` → `0.0`. + - `double getAverageClickDistance()` — live wrapper combining the two using the listener's points and the client's canvas size. + +- [ ] **Step 1: Write the failing tests** + +Create `src/test/java/com/afkstatstracker/AfkStatsTrackerCalcTest.java`: + +```java +package com.afkstatstracker; + +import java.awt.Point; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +public class AfkStatsTrackerCalcTest +{ + private final AfkStatsTrackerPlugin plugin = new AfkStatsTrackerPlugin(); + + @Test + public void testAverageDistanceEmpty() + { + assertEquals(0.0, plugin.computeAverageDistance(new ArrayList<>()), 1e-9); + } + + @Test + public void testAverageDistanceSinglePoint() + { + List points = Arrays.asList(new Point(5, 5)); + assertEquals(0.0, plugin.computeAverageDistance(points), 1e-9); + } + + @Test + public void testAverageDistanceTwoLegs() + { + // (0,0)->(3,4)=5, (3,4)->(3,4)=0 => avg 2.5 + List points = Arrays.asList(new Point(0, 0), new Point(3, 4), new Point(3, 4)); + assertEquals(2.5, plugin.computeAverageDistance(points), 1e-9); + } + + @Test + public void testAverageDistanceAxisAligned() + { + // (0,0)->(0,10)=10, (0,10)->(10,10)=10 => avg 10 + List points = Arrays.asList(new Point(0, 0), new Point(0, 10), new Point(10, 10)); + assertEquals(10.0, plugin.computeAverageDistance(points), 1e-9); + } + + @Test + public void testToDistancePercentZeroCanvas() + { + assertEquals(0.0, plugin.toDistancePercent(50, 0, 0), 1e-9); + } + + @Test + public void testToDistancePercentHalf() + { + // diagonal of 300x400 = 500; 50/500 = 10% + assertEquals(10.0, plugin.toDistancePercent(50, 300, 400), 1e-9); + } + + @Test + public void testToDistancePercentFullDiagonal() + { + // 500/500 = 100% + assertEquals(100.0, plugin.toDistancePercent(500, 300, 400), 1e-9); + } +} +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `nix-shell -p jdk11 --run "./gradlew test --tests com.afkstatstracker.AfkStatsTrackerCalcTest"` +Expected: FAIL — `cannot find symbol: method computeAverageDistance` / `toDistancePercent`. + +- [ ] **Step 3: Implement the helpers** + +In `AfkStatsTrackerPlugin.java`, add the import near the other imports: + +```java +import java.awt.Point; +``` + +Add these three methods inside the class (e.g. directly after `getAverageClickInterval()`): + +```java + public double computeAverageDistance(List points) + { + int n = points.size(); + if (n < 2) return 0.0; + + double sum = 0.0; + for (int i = 1; i < n; i++) + { + sum += points.get(i).distance(points.get(i - 1)); + } + return sum / (n - 1); + } + + public double toDistancePercent(double avgPixelDist, int canvasWidth, int canvasHeight) + { + double diagonal = Math.hypot(canvasWidth, canvasHeight); + if (diagonal <= 0) return 0.0; + return avgPixelDist / diagonal * 100.0; + } + + public double getAverageClickDistance() + { + double avgPx = computeAverageDistance(mouseListener.getClickPoints()); + return toDistancePercent(avgPx, client.getCanvasWidth(), client.getCanvasHeight()); + } +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `nix-shell -p jdk11 --run "./gradlew test --tests com.afkstatstracker.AfkStatsTrackerCalcTest"` +Expected: PASS (all 7 calc tests). + +- [ ] **Step 5: Commit** + +```bash +git add src/main/java/com/afkstatstracker/AfkStatsTrackerPlugin.java src/test/java/com/afkstatstracker/AfkStatsTrackerCalcTest.java +git commit -m "feat: compute average click distance as % of canvas diagonal" +``` + +--- + +### Task 3: Add `avgDistancePercent` to Session and wire it in + +**Files:** +- Modify: `src/main/java/com/afkstatstracker/Session.java` +- Modify: `src/main/java/com/afkstatstracker/AfkStatsTrackerPlugin.java` (`stopSession`) +- Test: `src/test/java/com/afkstatstracker/SessionTest.java` + +**Interfaces:** +- Consumes: `getAverageClickDistance()` (Task 2). +- Produces: `Session` constructor gains a trailing `double avgDistancePercent` arg; `double getAvgDistancePercent()` getter. + +> Note: this changes the `Session` constructor arity. The only other caller is `stopSession()` (updated in this task) and `SessionTest` (updated in this task). `SessionHistoryManager` deserializes via Gson and does not call the constructor directly — verify with a grep in Step 3. + +- [ ] **Step 1: Update the failing test** + +In `SessionTest.java`, update both `new Session(...)` calls to pass a trailing distance arg, and assert the getter in `testSessionCreation`. + +`testSessionCreation` — change the constructor call and add an assertion: + +```java + Session session = new Session( + "test-id", + "Test Session", + 1000L, + 2000L, + 42, + 85L, + 45000.0, + 12.5 + ); + + assertEquals("test-id", session.getId()); + assertEquals("Test Session", session.getName()); + assertEquals(1000L, session.getStartTime()); + assertEquals(2000L, session.getEndTime()); + assertEquals(42, session.getClickCount()); + assertEquals(85L, session.getConsistencyScore()); + assertEquals(45000.0, session.getAvgInterval(), 0.001); + assertEquals(12.5, session.getAvgDistancePercent(), 0.001); +``` + +`testSessionSetName` — change its constructor call to add a trailing `0.0`: + +```java + Session session = new Session( + "test-id", + "Original Name", + 1000L, + 2000L, + 10, + 50L, + 30000.0, + 0.0 + ); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `nix-shell -p jdk11 --run "./gradlew test --tests com.afkstatstracker.SessionTest"` +Expected: FAIL — constructor arity mismatch / `cannot find symbol: method getAvgDistancePercent`. + +- [ ] **Step 3: Implement the model change** + +First confirm the only direct constructor caller is `stopSession`: + +Run: `grep -rn "new Session(" src/main` +Expected: a single match in `AfkStatsTrackerPlugin.java` (`stopSession`). + +Replace the full contents of `Session.java` with: + +```java +package com.afkstatstracker; + +public class Session +{ + private final String id; + private String name; + private final long startTime; + private final long endTime; + private final int clickCount; + private final long consistencyScore; + private final double avgInterval; + private final double avgDistancePercent; + + public Session(String id, String name, long startTime, long endTime, + int clickCount, long consistencyScore, double avgInterval, + double avgDistancePercent) + { + this.id = id; + this.name = name; + this.startTime = startTime; + this.endTime = endTime; + this.clickCount = clickCount; + this.consistencyScore = consistencyScore; + this.avgInterval = avgInterval; + this.avgDistancePercent = avgDistancePercent; + } + + public String getId() { return id; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public long getStartTime() { return startTime; } + public long getEndTime() { return endTime; } + public int getClickCount() { return clickCount; } + public long getConsistencyScore() { return consistencyScore; } + public double getAvgInterval() { return avgInterval; } + public double getAvgDistancePercent() { return avgDistancePercent; } + +} +``` + +Then in `AfkStatsTrackerPlugin.java`, update the `Session` construction inside `stopSession()` to pass the new value as the final argument: + +```java + Session session = new Session( + id, + name, + startTime, + endTime, + getClickCount(), + getConsistency(), + getAverageClickInterval(), + getAverageClickDistance() + ); +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `nix-shell -p jdk11 --run "./gradlew test --tests com.afkstatstracker.SessionTest"` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add src/main/java/com/afkstatstracker/Session.java src/main/java/com/afkstatstracker/AfkStatsTrackerPlugin.java src/test/java/com/afkstatstracker/SessionTest.java +git commit -m "feat: store avg click distance on session" +``` + +--- + +### Task 4: Panel UI — stat card, indicator, history, clipboard + +**Files:** +- Modify: `src/main/java/com/afkstatstracker/AfkStatsTrackerPanel.java` + +**Interfaces:** +- Consumes: `plugin.getAverageClickDistance()` (Task 2); `Session.getAvgDistancePercent()` (Task 3). +- Produces: a new `DistanceIndicator` inner class and an `avgDistanceValueLabel` field; no external API. + +> No unit tests — the panel is Swing UI with no existing test coverage. Verification is a clean full build plus the existing test suite passing. + +- [ ] **Step 1: Add the field and the distance stat card** + +Add a field beside the other value labels (near line 48): + +```java + private JLabel avgDistanceValueLabel; +``` + +Add a field beside the other indicators (near line 53): + +```java + private DistanceIndicator distanceIndicator; +``` + +In the constructor, after the `avgIntervalPanel` block (around line 110), add: + +```java + JPanel avgDistancePanel = createStatCard("Avg Click Distance", + "Average distance the mouse moved between clicks, as a percentage of the canvas diagonal."); + avgDistanceValueLabel = (JLabel) ((BorderLayout) avgDistancePanel.getLayout()).getLayoutComponent(BorderLayout.CENTER); +``` + +Where `intervalIndicator` is constructed (around line 113), add: + +```java + distanceIndicator = new DistanceIndicator(); +``` + +In the `statsPanel.add(...)` sequence (around lines 115-119), add the new card and indicator after the interval indicator: + +```java + statsPanel.add(createSessionCard()); + statsPanel.add(consistencyPanel); + statsPanel.add(consistencyIndicator); + statsPanel.add(avgIntervalPanel); + statsPanel.add(intervalIndicator); + statsPanel.add(avgDistancePanel); + statsPanel.add(distanceIndicator); +``` + +- [ ] **Step 2: Update `updateStats()`** + +In `updateStats()` (around line 433), after the interval lines, add: + +```java + double avgDistance = plugin.getAverageClickDistance(); + avgDistanceValueLabel.setText(String.format("%.1f%%", avgDistance)); + distanceIndicator.setValue(avgDistance); +``` + +- [ ] **Step 3: Update the history row stats line and clipboard text** + +In `createSessionRow(...)`, change the `statsLabel` format (around line 275) to include distance: + +```java + JLabel statsLabel = new JLabel(String.format("%d | %.0fms | %.1f%% | %d clicks", + session.getConsistencyScore(), + session.getAvgInterval(), + session.getAvgDistancePercent(), + session.getClickCount())); +``` + +In the copy icon's click handler (around line 292), add an `Avg Distance` line to the copied text: + +```java + String text = String.format( + "Session: %s\nLength: %s\nConsistency: %d\nAvg Interval: %.0fms\nAvg Distance: %.1f%%\nClicks: %d", + session.getName(), lengthStr, + session.getConsistencyScore(), + session.getAvgInterval(), + session.getAvgDistancePercent(), + session.getClickCount()); +``` + +- [ ] **Step 4: Add the `DistanceIndicator` inner class** + +Add this inner class beside `ConsistencyIndicator` / `IntervalIndicator` (before the closing brace of `AfkStatsTrackerPanel`). It is a linear 0–100 slider taking a `double`, with ticks every 25 and `%` labels at 0 and 100: + +```java + private static class DistanceIndicator extends JPanel + { + private static final int MARKER_SIZE = 8; + private static final int PADDING_X = 12; + private static final int TICK_HEIGHT = 4; + private static final Color TRACK_COLOR = ColorScheme.MEDIUM_GRAY_COLOR; + private static final Color TICK_COLOR = ColorScheme.LIGHT_GRAY_COLOR; + private static final Color MARKER_COLOR = ColorScheme.BRAND_ORANGE; + + private double value = 0; + + DistanceIndicator() + { + setToolTipText("Average distance the mouse moved between clicks, as a percentage of the canvas diagonal."); + setOpaque(false); + setBorder(BorderFactory.createEmptyBorder(2, 5, 4, 5)); + setMaximumSize(new Dimension(Integer.MAX_VALUE, 36)); + setPreferredSize(new Dimension(0, 36)); + } + + void setValue(double value) + { + this.value = Math.max(0, Math.min(100, value)); + repaint(); + } + + @Override + protected void paintComponent(Graphics g) + { + super.paintComponent(g); + Graphics2D g2 = (Graphics2D) g.create(); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + int w = getWidth(); + + Font labelFont = g2.getFont().deriveFont(Font.PLAIN, 9f); + g2.setFont(labelFont); + int labelHeight = g2.getFontMetrics().getHeight(); + + int trackLeft = PADDING_X; + int trackRight = w - PADDING_X; + int trackWidth = trackRight - trackLeft; + int lineY = MARKER_SIZE + 2; + + // Draw horizontal line + g2.setColor(TRACK_COLOR); + g2.drawLine(trackLeft, lineY, trackRight, lineY); + + // Draw tick marks every 25 and labels at 0 and 100 + for (int i = 0; i <= 100; i += 25) + { + int x = trackLeft + (int) (trackWidth * i / 100.0); + g2.setColor(TICK_COLOR); + g2.drawLine(x, lineY - TICK_HEIGHT / 2, x, lineY + TICK_HEIGHT / 2); + + if (i == 0 || i == 100) + { + String label = i + "%"; + int labelWidth = g2.getFontMetrics().stringWidth(label); + g2.setColor(Color.GRAY); + g2.drawString(label, x - labelWidth / 2, lineY + TICK_HEIGHT / 2 + labelHeight); + } + } + + // Draw marker (triangle pointing down) + int markerX = trackLeft + (int) (trackWidth * value / 100.0); + int markerTop = lineY - MARKER_SIZE - 1; + int[] xPoints = {markerX - MARKER_SIZE / 2, markerX + MARKER_SIZE / 2, markerX}; + int[] yPoints = {markerTop, markerTop, lineY - 1}; + g2.setColor(MARKER_COLOR); + g2.fillPolygon(xPoints, yPoints, 3); + + g2.dispose(); + } + } +``` + +- [ ] **Step 5: Build and run the full test suite** + +Run: `nix-shell -p jdk11 --run "./gradlew build"` +Expected: `BUILD SUCCESSFUL`, all existing + new tests pass, panel compiles. + +- [ ] **Step 6: Commit** + +```bash +git add src/main/java/com/afkstatstracker/AfkStatsTrackerPanel.java +git commit -m "feat: show avg click distance card and indicator in panel" +``` + +--- + +### Task 5: Screenshot + README refresh + +**Files:** +- Modify: `panel-preview.png`, `README.md` (if it references the preview) + +> Per project workflow: panel UI changed, so the preview image and README must be refreshed and the screenshot included in the PR description. + +- [ ] **Step 1: Capture an updated panel screenshot** + +Ask the user for a fresh screenshot of the panel showing the new Avg Click Distance card + indicator (the agent cannot launch the RuneLite client to capture it). + +- [ ] **Step 2: Replace the preview image** + +Save the new screenshot over `panel-preview.png` in the repo root. Confirm `README.md`'s image reference still points at `panel-preview.png` (no change needed if the filename is unchanged). + +- [ ] **Step 3: Commit** + +```bash +git add panel-preview.png README.md +git commit -m "docs: update panel preview with avg click distance" +``` + +--- + +## Self-Review + +**Spec coverage:** +- Data capture (listener records points) → Task 1. ✓ +- `computeAverageDistance` + `toDistancePercent` + `getAverageClickDistance` → Task 2. ✓ +- `Session.avgDistancePercent` + `stopSession` wiring → Task 3. ✓ +- Stat card, `DistanceIndicator` (linear 0–100), `updateStats`, history line, clipboard → Task 4. ✓ +- Testing of both pure helpers → Task 2 (7 cases). ✓ +- Follow-up screenshot/README → Task 5. ✓ + +**Type consistency:** +- `getClickPoints(): List` produced in Task 1, consumed in Task 2. ✓ +- `computeAverageDistance(List)`, `toDistancePercent(double,int,int)`, `getAverageClickDistance()` named identically across Tasks 2–4. ✓ +- `Session` constructor trailing `double avgDistancePercent` + `getAvgDistancePercent()` — defined Task 3, consumed Task 4. ✓ +- Display format `"%.1f%%"` used in card, history line, and clipboard. ✓ + +**Placeholder scan:** none — all steps contain concrete code and exact commands. From 0451457e726a3b6f7a10ed1a5101684597b662a2 Mon Sep 17 00:00:00 2001 From: Reasel Date: Mon, 22 Jun 2026 16:09:33 -0700 Subject: [PATCH 4/4] docs: remove avg click distance planning docs --- .../2026-06-22-avg-click-distance-design.md | 120 ---- docs/plans/2026-06-22-avg-click-distance.md | 661 ------------------ 2 files changed, 781 deletions(-) delete mode 100644 docs/plans/2026-06-22-avg-click-distance-design.md delete mode 100644 docs/plans/2026-06-22-avg-click-distance.md diff --git a/docs/plans/2026-06-22-avg-click-distance-design.md b/docs/plans/2026-06-22-avg-click-distance-design.md deleted file mode 100644 index 454dfc4..0000000 --- a/docs/plans/2026-06-22-avg-click-distance-design.md +++ /dev/null @@ -1,120 +0,0 @@ -# Avg Click Distance Metric — Design - -## Goal - -Add a third live metric: **Average Click Distance** — the mean distance the mouse jumps between consecutive clicks, expressed as a **percentage of the game canvas diagonal**. Parallel to the existing Avg Click Interval metric (which measures *time* between clicks), this measures *distance* between clicks. Lets a user discuss how much they had to move their mouse during an AFK session, independent of resolution / display mode. - -## Definition - -For clicks recorded in time order with positions `p_1 … p_n` (pixels), and a canvas of width `w`, height `h`: - -``` -avgPixelDist = ( Σ distance(p_i, p_{i-1}) for i in 2..n ) / (n - 1) -diagonal = sqrt(w² + h²) -avgDistancePercent = avgPixelDist / diagonal × 100 -``` - -- `distance` = Euclidean: `sqrt(dx² + dy²)`. -- Normalized against the canvas **diagonal** — the maximum possible distance between two on-canvas points — so the value is a clean `0–100%` regardless of resolution or fixed/resizable mode. -- Points are in click order (time order), **not** sorted — distance is between consecutive clicks as they happened. -- Fewer than 2 clicks → `0.0`. Also `0.0` if `diagonal <= 0` (canvas not yet available, e.g. before login). Mirrors `getAverageClickInterval`'s `< 2` guard. -- Coordinates come from `MouseEvent.getPoint()`, relative to the game canvas. Canvas size comes from `client.getCanvasWidth()` / `client.getCanvasHeight()` (confirmed present in the RuneLite API). -- Caveat: if the canvas is resized mid-session, stored pixel distances are normalized by the canvas diagonal at compute time, introducing a small skew. Acceptable for an AFK stat; not corrected for. - -## Components - -### 1. Data capture — `MouseClickCounterListener` - -Add a parallel `List clickPoints` alongside the existing `List clickTimestamps`. - -- `mousePressed` records `mouseEvent.getPoint()` at the same call site that records the timestamp. -- New synchronized `getClickPoints()` returning a defensive copy. -- `resetMouseClickCounterListener()` clears both lists. - -Rationale: a parallel list keeps the existing `List` timestamp API and the `computeConsistency` / `getAverageClickInterval` signatures untouched. No `Click(ts,x,y)` record type — that would churn the existing methods for no gain. - -### 2. Calculation — `AfkStatsTrackerPlugin` - -Split into a pure, testable helper plus a thin live wrapper — mirrors how `computeConsistency(List)` is separated from `getConsistency()` so the math is unit-testable without a live client. - -Pure helper (no client dependency): - -``` -// Average Euclidean pixel distance between consecutive clicks. < 2 points → 0. -public double computeAverageDistance(List points) -{ - int n = points.size(); - if (n < 2) return 0.0; - double sum = 0.0; - for (int i = 1; i < n; i++) sum += points.get(i).distance(points.get(i - 1)); - return sum / (n - 1); -} - -// Normalize an average pixel distance to % of the canvas diagonal. diagonal <= 0 → 0. -public double toDistancePercent(double avgPixelDist, int canvasWidth, int canvasHeight) -{ - double diagonal = Math.hypot(canvasWidth, canvasHeight); - if (diagonal <= 0) return 0.0; - return avgPixelDist / diagonal * 100.0; -} -``` - -Live wrapper: - -``` -public double getAverageClickDistance() -{ - double avgPx = computeAverageDistance(mouseListener.getClickPoints()); - return toDistancePercent(avgPx, client.getCanvasWidth(), client.getCanvasHeight()); -} -``` - -Uses `java.awt.Point.distance(Point)` for the Euclidean calc and `Math.hypot` for the diagonal. No sorting (points already in click order). - -### 3. Model — `Session` - -Add `private final double avgDistancePercent;` with a getter, as the last constructor argument. `stopSession()` passes `getAverageClickDistance()`. - -Backward compatibility: sessions persisted before this change have no `avgDistancePercent` in their JSON; gson deserializes the field to `0.0`. These render as `0.0%`. Acceptable — no migration needed. - -### 4. UI — `AfkStatsTrackerPanel` - -- New **"Avg Click Distance"** stat card (clone of the Avg Click Interval card), value formatted `"%.1f%%"`. Tooltip: "Average distance the mouse moved between clicks, as a percentage of the canvas diagonal." -- New inner class **`DistanceIndicator`** — a linear `0–100` indicator. Model it on `ConsistencyIndicator` (already a linear 0–100 slider) rather than the logarithmic `IntervalIndicator`, but take a `double` value: - - Range `0–100%`. Ticks every `25` (`0 / 25 / 50 / 75 / 100`), labels at `0` and `100` with a `%` suffix. - - `setValue(double)` clamps to `[0, 100]`. -- Place the new card + indicator after the interval card/indicator in `statsPanel`. -- `updateStats()` reads `plugin.getAverageClickDistance()`, updates the card label (`"%.1f%%"`) and `distanceIndicator.setValue(...)`. -- History row stats line (`createSessionRow`): extend the format to include distance, e.g. `"%d | %.0fms | %.1f%% | %d clicks"`. -- Copy-to-clipboard text: add an `Avg Distance: %.1f%%` line. - -## Testing - -Extend the existing test suite, targeting the two pure helpers (no client mock needed): - -`computeAverageDistance(List)`: -- 0 points → `0.0` -- 1 point → `0.0` -- `(0,0) → (3,4) → (3,4)` → distances `5, 0` → avg `2.5` -- `(0,0) → (0,10) → (10,10)` → distances `10, 10` → avg `10.0` - -`toDistancePercent(avgPixelDist, w, h)`: -- `w = h = 0` (or either ≤ 0) → `0.0` -- `avgPixelDist = 50`, canvas `300×400` (diagonal `500`) → `10.0` -- `avgPixelDist = 500`, canvas `300×400` → `100.0` (full-diagonal move) - -Use a small delta for double assertions (e.g. `assertEquals(expected, actual, 1e-9)`). - -## Scope - -Files touched: `MouseClickCounterListener`, `AfkStatsTrackerPlugin`, `Session`, `AfkStatsTrackerPanel`, plus the test class. One new inner class (`DistanceIndicator`). - -## Out of scope (YAGNI) - -- Tracking the *full* mouse path (every `mouseMoved`/`mouseDragged`) — heavier, not what was asked. -- A consistency-style score for distance — only the average was requested. -- Correcting for mid-session canvas resize — normalize at compute time, accept the small skew. - -## Follow-up - -Panel UI changes → after implementation, capture an updated panel screenshot, refresh `panel-preview.png` + README reference, and include it in the PR description. diff --git a/docs/plans/2026-06-22-avg-click-distance.md b/docs/plans/2026-06-22-avg-click-distance.md deleted file mode 100644 index b9b103f..0000000 --- a/docs/plans/2026-06-22-avg-click-distance.md +++ /dev/null @@ -1,661 +0,0 @@ -# Avg Click Distance Metric Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add a live "Avg Click Distance" metric — mean mouse-jump between consecutive clicks, shown as a percentage of the game canvas diagonal — alongside the existing consistency and interval metrics. - -**Architecture:** The mouse listener records each click's `(x,y)` point in parallel with its timestamp. The plugin computes the average Euclidean pixel distance between consecutive clicks (pure helper), then normalizes it against the canvas diagonal to a 0–100% value (second pure helper). The `Session` model gains an `avgDistancePercent` field; the panel gains a stat card and a linear 0–100 `DistanceIndicator`. - -**Tech Stack:** Java 11, RuneLite plugin API (`net.runelite.api.Client`, `MouseManager`), Swing panel UI, JUnit 4, Lombok, Gson. Build via `nix-shell -p jdk11 --run "./gradlew ..."`. - -## Global Constraints - -- Java 11 target. Build/test commands MUST be wrapped: `nix-shell -p jdk11 --run "./gradlew "`. -- The new metric is a **percentage of the canvas diagonal**, range `0–100`, displayed as `"%.1f%%"`. -- Distance points are kept in **click (time) order — never sorted**. -- Fewer than 2 clicks, or canvas diagonal `<= 0`, → `0.0`. -- Follow existing code style: no Lombok on the touched classes beyond what's already there, 4-space indentation matching each file. - ---- - -### Task 1: Capture click coordinates in the listener - -**Files:** -- Modify: `src/main/java/com/afkstatstracker/MouseClickCounterListener.java` -- Test: `src/test/java/com/afkstatstracker/MouseClickCounterListenerTest.java` - -**Interfaces:** -- Consumes: nothing new. -- Produces: - - `void addClick(int x, int y)` — records a timestamp **and** a `Point(x,y)`. - - `void addClick()` — unchanged signature; now delegates to `addClick(0, 0)` so the timestamp and point lists stay equal length. - - `List getClickPoints()` — defensive copy, in click order. - - `mousePressed` records `mouseEvent.getX()`, `mouseEvent.getY()`. - - `resetMouseClickCounterListener()` clears points too. - -- [ ] **Step 1: Write the failing tests** - -Add to `MouseClickCounterListenerTest.java` (add imports `java.awt.Point` and keep existing ones): - -```java - @Test - public void testAddClickWithCoordsRecordsPoint() - { - MouseClickCounterListener listener = new MouseClickCounterListener(); - listener.addClick(10, 20); - listener.addClick(30, 40); - List points = listener.getClickPoints(); - assertEquals(2, points.size()); - assertEquals(new Point(10, 20), points.get(0)); - assertEquals(new Point(30, 40), points.get(1)); - } - - @Test - public void testNoArgAddClickKeepsListsParallel() - { - MouseClickCounterListener listener = new MouseClickCounterListener(); - listener.addClick(); - assertEquals(1, listener.getClickCounter().size()); - assertEquals(1, listener.getClickPoints().size()); - } - - @Test - public void testResetClearsPoints() - { - MouseClickCounterListener listener = new MouseClickCounterListener(); - listener.addClick(5, 5); - listener.resetMouseClickCounterListener(); - assertTrue(listener.getClickPoints().isEmpty()); - } - - @Test - public void testGetClickPointsReturnsDefensiveCopy() - { - MouseClickCounterListener listener = new MouseClickCounterListener(); - listener.addClick(1, 1); - List copy = listener.getClickPoints(); - copy.clear(); - assertEquals(1, listener.getClickPoints().size()); - } -``` - -Add the import at the top of the test file: - -```java -import java.awt.Point; -``` - -- [ ] **Step 2: Run tests to verify they fail** - -Run: `nix-shell -p jdk11 --run "./gradlew test --tests com.afkstatstracker.MouseClickCounterListenerTest"` -Expected: FAIL — compile error / `cannot find symbol: method getClickPoints()` and `addClick(int,int)`. - -- [ ] **Step 3: Implement the change** - -Replace the full contents of `MouseClickCounterListener.java` with: - -```java -package com.afkstatstracker; - -import java.awt.Point; -import java.awt.event.MouseEvent; -import java.util.ArrayList; -import java.util.List; -import net.runelite.client.input.MouseAdapter; - -public class MouseClickCounterListener extends MouseAdapter -{ - private final List clickTimestamps = new ArrayList<>(); - private final List clickPoints = new ArrayList<>(); - - @Override - public MouseEvent mousePressed(MouseEvent mouseEvent) - { - addClick(mouseEvent.getX(), mouseEvent.getY()); - return mouseEvent; - } - - public synchronized List getClickCounter() - { - return new ArrayList<>(clickTimestamps); - } - - public synchronized List getClickPoints() - { - return new ArrayList<>(clickPoints); - } - - public synchronized void addClick() - { - addClick(0, 0); - } - - public synchronized void addClick(int x, int y) - { - clickTimestamps.add(System.currentTimeMillis()); - clickPoints.add(new Point(x, y)); - } - - public synchronized void resetMouseClickCounterListener() - { - this.clickTimestamps.clear(); - this.clickPoints.clear(); - } -} -``` - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `nix-shell -p jdk11 --run "./gradlew test --tests com.afkstatstracker.MouseClickCounterListenerTest"` -Expected: PASS (all listener tests, old and new). - -- [ ] **Step 5: Commit** - -```bash -git add src/main/java/com/afkstatstracker/MouseClickCounterListener.java src/test/java/com/afkstatstracker/MouseClickCounterListenerTest.java -git commit -m "feat: record click coordinates in mouse listener" -``` - ---- - -### Task 2: Distance calculation helpers in the plugin - -**Files:** -- Modify: `src/main/java/com/afkstatstracker/AfkStatsTrackerPlugin.java` -- Test: `src/test/java/com/afkstatstracker/AfkStatsTrackerCalcTest.java` (create) - -**Interfaces:** -- Consumes: `MouseClickCounterListener.getClickPoints()` (Task 1); `client.getCanvasWidth()`, `client.getCanvasHeight()`. -- Produces: - - `double computeAverageDistance(List points)` — avg Euclidean pixel distance between consecutive points; `< 2` → `0.0`. - - `double toDistancePercent(double avgPixelDist, int canvasWidth, int canvasHeight)` — `avgPixelDist / hypot(w,h) * 100`; diagonal `<= 0` → `0.0`. - - `double getAverageClickDistance()` — live wrapper combining the two using the listener's points and the client's canvas size. - -- [ ] **Step 1: Write the failing tests** - -Create `src/test/java/com/afkstatstracker/AfkStatsTrackerCalcTest.java`: - -```java -package com.afkstatstracker; - -import java.awt.Point; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import org.junit.Test; -import static org.junit.Assert.assertEquals; - -public class AfkStatsTrackerCalcTest -{ - private final AfkStatsTrackerPlugin plugin = new AfkStatsTrackerPlugin(); - - @Test - public void testAverageDistanceEmpty() - { - assertEquals(0.0, plugin.computeAverageDistance(new ArrayList<>()), 1e-9); - } - - @Test - public void testAverageDistanceSinglePoint() - { - List points = Arrays.asList(new Point(5, 5)); - assertEquals(0.0, plugin.computeAverageDistance(points), 1e-9); - } - - @Test - public void testAverageDistanceTwoLegs() - { - // (0,0)->(3,4)=5, (3,4)->(3,4)=0 => avg 2.5 - List points = Arrays.asList(new Point(0, 0), new Point(3, 4), new Point(3, 4)); - assertEquals(2.5, plugin.computeAverageDistance(points), 1e-9); - } - - @Test - public void testAverageDistanceAxisAligned() - { - // (0,0)->(0,10)=10, (0,10)->(10,10)=10 => avg 10 - List points = Arrays.asList(new Point(0, 0), new Point(0, 10), new Point(10, 10)); - assertEquals(10.0, plugin.computeAverageDistance(points), 1e-9); - } - - @Test - public void testToDistancePercentZeroCanvas() - { - assertEquals(0.0, plugin.toDistancePercent(50, 0, 0), 1e-9); - } - - @Test - public void testToDistancePercentHalf() - { - // diagonal of 300x400 = 500; 50/500 = 10% - assertEquals(10.0, plugin.toDistancePercent(50, 300, 400), 1e-9); - } - - @Test - public void testToDistancePercentFullDiagonal() - { - // 500/500 = 100% - assertEquals(100.0, plugin.toDistancePercent(500, 300, 400), 1e-9); - } -} -``` - -- [ ] **Step 2: Run tests to verify they fail** - -Run: `nix-shell -p jdk11 --run "./gradlew test --tests com.afkstatstracker.AfkStatsTrackerCalcTest"` -Expected: FAIL — `cannot find symbol: method computeAverageDistance` / `toDistancePercent`. - -- [ ] **Step 3: Implement the helpers** - -In `AfkStatsTrackerPlugin.java`, add the import near the other imports: - -```java -import java.awt.Point; -``` - -Add these three methods inside the class (e.g. directly after `getAverageClickInterval()`): - -```java - public double computeAverageDistance(List points) - { - int n = points.size(); - if (n < 2) return 0.0; - - double sum = 0.0; - for (int i = 1; i < n; i++) - { - sum += points.get(i).distance(points.get(i - 1)); - } - return sum / (n - 1); - } - - public double toDistancePercent(double avgPixelDist, int canvasWidth, int canvasHeight) - { - double diagonal = Math.hypot(canvasWidth, canvasHeight); - if (diagonal <= 0) return 0.0; - return avgPixelDist / diagonal * 100.0; - } - - public double getAverageClickDistance() - { - double avgPx = computeAverageDistance(mouseListener.getClickPoints()); - return toDistancePercent(avgPx, client.getCanvasWidth(), client.getCanvasHeight()); - } -``` - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `nix-shell -p jdk11 --run "./gradlew test --tests com.afkstatstracker.AfkStatsTrackerCalcTest"` -Expected: PASS (all 7 calc tests). - -- [ ] **Step 5: Commit** - -```bash -git add src/main/java/com/afkstatstracker/AfkStatsTrackerPlugin.java src/test/java/com/afkstatstracker/AfkStatsTrackerCalcTest.java -git commit -m "feat: compute average click distance as % of canvas diagonal" -``` - ---- - -### Task 3: Add `avgDistancePercent` to Session and wire it in - -**Files:** -- Modify: `src/main/java/com/afkstatstracker/Session.java` -- Modify: `src/main/java/com/afkstatstracker/AfkStatsTrackerPlugin.java` (`stopSession`) -- Test: `src/test/java/com/afkstatstracker/SessionTest.java` - -**Interfaces:** -- Consumes: `getAverageClickDistance()` (Task 2). -- Produces: `Session` constructor gains a trailing `double avgDistancePercent` arg; `double getAvgDistancePercent()` getter. - -> Note: this changes the `Session` constructor arity. The only other caller is `stopSession()` (updated in this task) and `SessionTest` (updated in this task). `SessionHistoryManager` deserializes via Gson and does not call the constructor directly — verify with a grep in Step 3. - -- [ ] **Step 1: Update the failing test** - -In `SessionTest.java`, update both `new Session(...)` calls to pass a trailing distance arg, and assert the getter in `testSessionCreation`. - -`testSessionCreation` — change the constructor call and add an assertion: - -```java - Session session = new Session( - "test-id", - "Test Session", - 1000L, - 2000L, - 42, - 85L, - 45000.0, - 12.5 - ); - - assertEquals("test-id", session.getId()); - assertEquals("Test Session", session.getName()); - assertEquals(1000L, session.getStartTime()); - assertEquals(2000L, session.getEndTime()); - assertEquals(42, session.getClickCount()); - assertEquals(85L, session.getConsistencyScore()); - assertEquals(45000.0, session.getAvgInterval(), 0.001); - assertEquals(12.5, session.getAvgDistancePercent(), 0.001); -``` - -`testSessionSetName` — change its constructor call to add a trailing `0.0`: - -```java - Session session = new Session( - "test-id", - "Original Name", - 1000L, - 2000L, - 10, - 50L, - 30000.0, - 0.0 - ); -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `nix-shell -p jdk11 --run "./gradlew test --tests com.afkstatstracker.SessionTest"` -Expected: FAIL — constructor arity mismatch / `cannot find symbol: method getAvgDistancePercent`. - -- [ ] **Step 3: Implement the model change** - -First confirm the only direct constructor caller is `stopSession`: - -Run: `grep -rn "new Session(" src/main` -Expected: a single match in `AfkStatsTrackerPlugin.java` (`stopSession`). - -Replace the full contents of `Session.java` with: - -```java -package com.afkstatstracker; - -public class Session -{ - private final String id; - private String name; - private final long startTime; - private final long endTime; - private final int clickCount; - private final long consistencyScore; - private final double avgInterval; - private final double avgDistancePercent; - - public Session(String id, String name, long startTime, long endTime, - int clickCount, long consistencyScore, double avgInterval, - double avgDistancePercent) - { - this.id = id; - this.name = name; - this.startTime = startTime; - this.endTime = endTime; - this.clickCount = clickCount; - this.consistencyScore = consistencyScore; - this.avgInterval = avgInterval; - this.avgDistancePercent = avgDistancePercent; - } - - public String getId() { return id; } - public String getName() { return name; } - public void setName(String name) { this.name = name; } - public long getStartTime() { return startTime; } - public long getEndTime() { return endTime; } - public int getClickCount() { return clickCount; } - public long getConsistencyScore() { return consistencyScore; } - public double getAvgInterval() { return avgInterval; } - public double getAvgDistancePercent() { return avgDistancePercent; } - -} -``` - -Then in `AfkStatsTrackerPlugin.java`, update the `Session` construction inside `stopSession()` to pass the new value as the final argument: - -```java - Session session = new Session( - id, - name, - startTime, - endTime, - getClickCount(), - getConsistency(), - getAverageClickInterval(), - getAverageClickDistance() - ); -``` - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `nix-shell -p jdk11 --run "./gradlew test --tests com.afkstatstracker.SessionTest"` -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add src/main/java/com/afkstatstracker/Session.java src/main/java/com/afkstatstracker/AfkStatsTrackerPlugin.java src/test/java/com/afkstatstracker/SessionTest.java -git commit -m "feat: store avg click distance on session" -``` - ---- - -### Task 4: Panel UI — stat card, indicator, history, clipboard - -**Files:** -- Modify: `src/main/java/com/afkstatstracker/AfkStatsTrackerPanel.java` - -**Interfaces:** -- Consumes: `plugin.getAverageClickDistance()` (Task 2); `Session.getAvgDistancePercent()` (Task 3). -- Produces: a new `DistanceIndicator` inner class and an `avgDistanceValueLabel` field; no external API. - -> No unit tests — the panel is Swing UI with no existing test coverage. Verification is a clean full build plus the existing test suite passing. - -- [ ] **Step 1: Add the field and the distance stat card** - -Add a field beside the other value labels (near line 48): - -```java - private JLabel avgDistanceValueLabel; -``` - -Add a field beside the other indicators (near line 53): - -```java - private DistanceIndicator distanceIndicator; -``` - -In the constructor, after the `avgIntervalPanel` block (around line 110), add: - -```java - JPanel avgDistancePanel = createStatCard("Avg Click Distance", - "Average distance the mouse moved between clicks, as a percentage of the canvas diagonal."); - avgDistanceValueLabel = (JLabel) ((BorderLayout) avgDistancePanel.getLayout()).getLayoutComponent(BorderLayout.CENTER); -``` - -Where `intervalIndicator` is constructed (around line 113), add: - -```java - distanceIndicator = new DistanceIndicator(); -``` - -In the `statsPanel.add(...)` sequence (around lines 115-119), add the new card and indicator after the interval indicator: - -```java - statsPanel.add(createSessionCard()); - statsPanel.add(consistencyPanel); - statsPanel.add(consistencyIndicator); - statsPanel.add(avgIntervalPanel); - statsPanel.add(intervalIndicator); - statsPanel.add(avgDistancePanel); - statsPanel.add(distanceIndicator); -``` - -- [ ] **Step 2: Update `updateStats()`** - -In `updateStats()` (around line 433), after the interval lines, add: - -```java - double avgDistance = plugin.getAverageClickDistance(); - avgDistanceValueLabel.setText(String.format("%.1f%%", avgDistance)); - distanceIndicator.setValue(avgDistance); -``` - -- [ ] **Step 3: Update the history row stats line and clipboard text** - -In `createSessionRow(...)`, change the `statsLabel` format (around line 275) to include distance: - -```java - JLabel statsLabel = new JLabel(String.format("%d | %.0fms | %.1f%% | %d clicks", - session.getConsistencyScore(), - session.getAvgInterval(), - session.getAvgDistancePercent(), - session.getClickCount())); -``` - -In the copy icon's click handler (around line 292), add an `Avg Distance` line to the copied text: - -```java - String text = String.format( - "Session: %s\nLength: %s\nConsistency: %d\nAvg Interval: %.0fms\nAvg Distance: %.1f%%\nClicks: %d", - session.getName(), lengthStr, - session.getConsistencyScore(), - session.getAvgInterval(), - session.getAvgDistancePercent(), - session.getClickCount()); -``` - -- [ ] **Step 4: Add the `DistanceIndicator` inner class** - -Add this inner class beside `ConsistencyIndicator` / `IntervalIndicator` (before the closing brace of `AfkStatsTrackerPanel`). It is a linear 0–100 slider taking a `double`, with ticks every 25 and `%` labels at 0 and 100: - -```java - private static class DistanceIndicator extends JPanel - { - private static final int MARKER_SIZE = 8; - private static final int PADDING_X = 12; - private static final int TICK_HEIGHT = 4; - private static final Color TRACK_COLOR = ColorScheme.MEDIUM_GRAY_COLOR; - private static final Color TICK_COLOR = ColorScheme.LIGHT_GRAY_COLOR; - private static final Color MARKER_COLOR = ColorScheme.BRAND_ORANGE; - - private double value = 0; - - DistanceIndicator() - { - setToolTipText("Average distance the mouse moved between clicks, as a percentage of the canvas diagonal."); - setOpaque(false); - setBorder(BorderFactory.createEmptyBorder(2, 5, 4, 5)); - setMaximumSize(new Dimension(Integer.MAX_VALUE, 36)); - setPreferredSize(new Dimension(0, 36)); - } - - void setValue(double value) - { - this.value = Math.max(0, Math.min(100, value)); - repaint(); - } - - @Override - protected void paintComponent(Graphics g) - { - super.paintComponent(g); - Graphics2D g2 = (Graphics2D) g.create(); - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - int w = getWidth(); - - Font labelFont = g2.getFont().deriveFont(Font.PLAIN, 9f); - g2.setFont(labelFont); - int labelHeight = g2.getFontMetrics().getHeight(); - - int trackLeft = PADDING_X; - int trackRight = w - PADDING_X; - int trackWidth = trackRight - trackLeft; - int lineY = MARKER_SIZE + 2; - - // Draw horizontal line - g2.setColor(TRACK_COLOR); - g2.drawLine(trackLeft, lineY, trackRight, lineY); - - // Draw tick marks every 25 and labels at 0 and 100 - for (int i = 0; i <= 100; i += 25) - { - int x = trackLeft + (int) (trackWidth * i / 100.0); - g2.setColor(TICK_COLOR); - g2.drawLine(x, lineY - TICK_HEIGHT / 2, x, lineY + TICK_HEIGHT / 2); - - if (i == 0 || i == 100) - { - String label = i + "%"; - int labelWidth = g2.getFontMetrics().stringWidth(label); - g2.setColor(Color.GRAY); - g2.drawString(label, x - labelWidth / 2, lineY + TICK_HEIGHT / 2 + labelHeight); - } - } - - // Draw marker (triangle pointing down) - int markerX = trackLeft + (int) (trackWidth * value / 100.0); - int markerTop = lineY - MARKER_SIZE - 1; - int[] xPoints = {markerX - MARKER_SIZE / 2, markerX + MARKER_SIZE / 2, markerX}; - int[] yPoints = {markerTop, markerTop, lineY - 1}; - g2.setColor(MARKER_COLOR); - g2.fillPolygon(xPoints, yPoints, 3); - - g2.dispose(); - } - } -``` - -- [ ] **Step 5: Build and run the full test suite** - -Run: `nix-shell -p jdk11 --run "./gradlew build"` -Expected: `BUILD SUCCESSFUL`, all existing + new tests pass, panel compiles. - -- [ ] **Step 6: Commit** - -```bash -git add src/main/java/com/afkstatstracker/AfkStatsTrackerPanel.java -git commit -m "feat: show avg click distance card and indicator in panel" -``` - ---- - -### Task 5: Screenshot + README refresh - -**Files:** -- Modify: `panel-preview.png`, `README.md` (if it references the preview) - -> Per project workflow: panel UI changed, so the preview image and README must be refreshed and the screenshot included in the PR description. - -- [ ] **Step 1: Capture an updated panel screenshot** - -Ask the user for a fresh screenshot of the panel showing the new Avg Click Distance card + indicator (the agent cannot launch the RuneLite client to capture it). - -- [ ] **Step 2: Replace the preview image** - -Save the new screenshot over `panel-preview.png` in the repo root. Confirm `README.md`'s image reference still points at `panel-preview.png` (no change needed if the filename is unchanged). - -- [ ] **Step 3: Commit** - -```bash -git add panel-preview.png README.md -git commit -m "docs: update panel preview with avg click distance" -``` - ---- - -## Self-Review - -**Spec coverage:** -- Data capture (listener records points) → Task 1. ✓ -- `computeAverageDistance` + `toDistancePercent` + `getAverageClickDistance` → Task 2. ✓ -- `Session.avgDistancePercent` + `stopSession` wiring → Task 3. ✓ -- Stat card, `DistanceIndicator` (linear 0–100), `updateStats`, history line, clipboard → Task 4. ✓ -- Testing of both pure helpers → Task 2 (7 cases). ✓ -- Follow-up screenshot/README → Task 5. ✓ - -**Type consistency:** -- `getClickPoints(): List` produced in Task 1, consumed in Task 2. ✓ -- `computeAverageDistance(List)`, `toDistancePercent(double,int,int)`, `getAverageClickDistance()` named identically across Tasks 2–4. ✓ -- `Session` constructor trailing `double avgDistancePercent` + `getAvgDistancePercent()` — defined Task 3, consumed Task 4. ✓ -- Display format `"%.1f%%"` used in card, history line, and clipboard. ✓ - -**Placeholder scan:** none — all steps contain concrete code and exact commands.