-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMetricBaselineService.lua
More file actions
90 lines (74 loc) · 3.31 KB
/
MetricBaselineService.lua
File metadata and controls
90 lines (74 loc) · 3.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
local _, ns = ...
-- Math is bound at file scope: Utils\Math.lua loads before this file in the
-- .toc. The framework only RegisterModule's modules (it does not call
-- OnInitialize), so the previous OnInitialize-based binding never ran and
-- left Math nil — a latent runtime crash in RecordMetric.
local Math = ns.Math
local MetricBaselineService = {}
local ROLLING_WINDOW_SIZE = 20
local DEFAULT_PRIOR_WEIGHT = 7
-- Population defaults per metric (fallback when player has no history)
local POPULATION_DEFAULTS = {
pressureScore = 55,
burstScore = 45,
survivabilityScore = 60,
rotationConsistencyScore = 50,
ccUptimePct = 0.20,
avoidableDamagePct = 0.30,
}
function MetricBaselineService:GetBaselinesDB()
local ok, db = pcall(function()
return ns.Addon:GetModule("CombatStore"):GetDB()
end)
if not ok or not db or not db.aggregates then return nil end
db.aggregates.metricBaselines = db.aggregates.metricBaselines or {}
return db.aggregates.metricBaselines
end
function MetricBaselineService:RecordMetric(context, metricName, value)
if not context or not metricName or not value then return end
local baselines = self:GetBaselinesDB()
if not baselines then return end
local contextKey = tostring(context):lower()
baselines[contextKey] = baselines[contextKey] or {}
local bucket = baselines[contextKey][metricName]
if not bucket then
bucket = { values = {}, count = 0, mean = 0, stdDev = 0, p25 = 0, p50 = 0, p75 = 0, lastUpdated = 0 }
baselines[contextKey][metricName] = bucket
end
-- FIFO insert
table.insert(bucket.values, value)
if #bucket.values > ROLLING_WINDOW_SIZE then
table.remove(bucket.values, 1)
end
bucket.count = (bucket.count or 0) + 1
-- Recompute stats
bucket.mean = Math.Average(bucket.values)
bucket.stdDev = Math.StandardDeviation(bucket.values)
bucket.p25 = Math.Percentile(bucket.values, 0.25)
bucket.p50 = Math.Percentile(bucket.values, 0.50)
bucket.p75 = Math.Percentile(bucket.values, 0.75)
bucket.lastUpdated = ns.ApiCompat and ns.ApiCompat.GetServerTime() or time()
end
function MetricBaselineService:GetThreshold(context, metricName, percentileKey)
percentileKey = percentileKey or "p25"
local baselines = self:GetBaselinesDB()
local contextKey = tostring(context or ""):lower()
local bucket = baselines and baselines[contextKey] and baselines[contextKey][metricName]
local populationDefault = POPULATION_DEFAULTS[metricName] or 50
if not bucket or not bucket.values or #bucket.values < 1 then
return populationDefault
end
local playerValue = bucket[percentileKey] or bucket.p25 or bucket.mean or 0
local sampleCount = #bucket.values
return Math.BayesianShrinkage(playerValue, sampleCount, populationDefault, DEFAULT_PRIOR_WEIGHT)
end
function MetricBaselineService:GetBaseline(context, metricName)
local baselines = self:GetBaselinesDB()
local contextKey = tostring(context or ""):lower()
return baselines and baselines[contextKey] and baselines[contextKey][metricName]
end
function MetricBaselineService:GetSampleCount(context, metricName)
local baseline = self:GetBaseline(context, metricName)
return baseline and baseline.count or 0
end
ns.Addon:RegisterModule("MetricBaselineService", MetricBaselineService)