Files
Freewood-OctoWoW-Codex/CodexZones.lua
T
2026-10-27 14:09:50 +00:00

238 lines
8.0 KiB
Lua

-- Freewood-OctoWoW-Codex: Gebiete mit Stufenspanne, berechnet aus den gesammelten Kreaturen.
--
-- Jede Kreatur zaehlt mit ihren Sichtungen (Zellen) in den Karten, in denen sie gesehen wurde, Instanzen mit ihrer
-- Instanz-ID (negative Zone). Pro Gebiet:
-- lo, hi typische Spanne: alle Stufen, die mindestens 5 % des Gewichts normaler Kreaturen haben
-- avg gewichteter Durchschnitt innerhalb der Spanne
-- out Ausreisser: normale Kreaturen ausserhalb der Spanne
-- special Elite / Selten / Boss usw. (fliessen nicht in die Spanne ein)
-- few zu wenig Daten (weniger als MIN_UNITS normale Kreaturen): Spanne nur grob
-- manual Spanne von Berechtigten von Hand gesetzt (db.ied, siehe Codex.ZoneEdID)
-- Wird mit den geteilten Kreaturdaten berechnet, daher sehen alle dasselbe.
local ADDON, NS = ...
local FW = _G.Freewood
if not FW or not FW:RequireAPI(1, ADDON) then
return
end
local Codex = NS and NS.Codex
if not Codex then
return
end
local Zones = {}
Codex.Zones = Zones
local GRID = Codex.GRID or 200
local MIN_UNITS = 3 -- weniger normale Kreaturen: "zu wenig Daten"
local LEVEL_SHARE = 0.05 -- Mindestanteil einer Stufe am Gewicht, damit sie zur typischen Spanne gehoert
local WEIGHT_CAP = 30 -- eine haeufig gesehene Kreatur zaehlt hoechstens so viel
local MAP_SHARE = 0.10 -- Karte zaehlt fuer eine Kreatur erst ab diesem Anteil ihrer Sichtungen
local CACHE_SECS = 5
local cache, cacheAt, dirty = nil, 0, true
function Zones.Invalidate()
dirty = true
end
FW:On("CODEX_CHANGED", function() dirty = true end, Zones)
local function Now()
return GetTime and GetTime() or 0
end
-- Gewichte einer Kreatur je Gebiet: { [zoneKey] = Gewicht }
local function Spread(u)
local out = {}
if type(u.ez) == "table" and #u.ez > 0 then
local each = 1 / #u.ez
local total = 0
for _, n in pairs(u.cells or {}) do total = total + n end
total = math.max(1, total)
for _, m in ipairs(u.ez) do out[m] = total * each end
return out
end
if type(u.iz) == "table" and next(u.iz) and u.dg ~= false then
for id, n in pairs(u.iz) do out[-id] = n end
return out
end
local per, total = {}, 0
for key, n in pairs(u.cells or {}) do
local m = math.floor(key / (GRID * GRID))
per[m] = (per[m] or 0) + n
total = total + n
end
for m, n in pairs(per) do
if total > 0 and n / total >= MAP_SHARE then out[m] = n end
end
return out
end
local function ManualRange(ied, key)
local rec = ied and ied[Codex.ZoneEdID(key)]
if not rec then return nil end
local l1 = rec.l1 or (rec.ed and rec.ed.l1)
local l2 = rec.l2 or (rec.ed and rec.ed.l2)
if l1 then return l1, l2 or l1 end
return nil
end
local function Compute()
local V = Codex.V()
local zones = {}
local function Zone(key)
local z = zones[key]
if not z then
z = { id = key, normal = {}, special = {}, out = {} }
zones[key] = z
end
return z
end
for id, u in pairs(V.units or {}) do
if u.l1 and u.bad ~= true then
local special = u.c ~= nil and u.c ~= false
local l2 = u.l2 or u.l1
for key, n in pairs(Spread(u)) do
local w = math.max(1, math.min(WEIGHT_CAP, n))
local entry = { id = id, l1 = u.l1, l2 = l2, w = w, c = special and u.c or nil, mid = (u.l1 + l2) / 2 }
local z = Zone(key)
if special then
z.special[#z.special + 1] = entry
else
z.normal[#z.normal + 1] = entry
end
end
end
end
for key, z in pairs(zones) do
local byLevel, total = {}, 0
local minL, maxL
for _, e in ipairs(z.normal) do
-- eine Kreatur mit Spanne (Stufe 3-4) zaehlt je zur Haelfte fuer beide Stufen
local a, b = e.l1, e.l2
local half = e.w / ((b > a) and 2 or 1)
byLevel[a] = (byLevel[a] or 0) + half
if b > a then byLevel[b] = (byLevel[b] or 0) + half end
total = total + e.w
minL = minL and math.min(minL, a) or a
maxL = maxL and math.max(maxL, b) or b
end
z.units = #z.normal
z.few = z.units < MIN_UNITS
local lo, hi
if total > 0 then
for lv, w in pairs(byLevel) do
if w / total >= LEVEL_SHARE then
lo = lo and math.min(lo, lv) or lv
hi = hi and math.max(hi, lv) or lv
end
end
lo, hi = lo or minL, hi or maxL
elseif #z.special > 0 then
for _, e in ipairs(z.special) do
minL = minL and math.min(minL, e.l1) or e.l1
maxL = maxL and math.max(maxL, e.l2) or e.l2
end
lo, hi = minL, maxL
z.few = true
end
local ml, mh = ManualRange(V.ied, key)
if ml then
lo, hi = ml, mh
z.manual = true
z.few = false
end
z.lo, z.hi = lo, hi
local sum, wsum = 0, 0
for _, e in ipairs(z.normal) do
if lo and e.mid >= lo and e.mid <= hi then
sum, wsum = sum + e.mid * e.w, wsum + e.w
end
end
z.avg = wsum > 0 and sum / wsum or ((lo and hi) and (lo + hi) / 2 or nil)
for _, e in ipairs(z.normal) do
if lo and (e.l2 < lo or e.l1 > hi) then z.out[#z.out + 1] = e end
end
z.dungeon = key < 0
z.minL, z.maxL = minL, maxL
end
-- von Hand gesetzte Spannen auch fuer Gebiete ohne Kreaturdaten
for edID, rec in pairs(V.ied or {}) do
local key = Codex.ZoneFromEdID(edID)
if key and not zones[key] then
local l1, l2 = ManualRange(V.ied, key)
if l1 then
zones[key] = { id = key, normal = {}, special = {}, out = {}, units = 0, lo = l1, hi = l2, avg = (l1 + l2) / 2,
manual = true, dungeon = key < 0 }
end
end
end
return zones
end
-- Alle Gebiete: { [zoneKey] = Daten }. Kurz zwischengespeichert.
function Zones.All()
local t = Now()
if not cache or (dirty and t - cacheAt >= CACHE_SECS) or t < cacheAt then
local ok, res = pcall(Compute)
cache = ok and res or cache or {}
cacheAt, dirty = t, false
end
return cache
end
-- Sofort neu berechnen (zum Beispiel nach einer Aenderung im Fenster oder in Tests)
function Zones.Refresh()
dirty = true
cacheAt = -1e9
return Zones.All()
end
function Zones.Get(key)
return Zones.All()[key]
end
-- Eine Kreatur gehoert zu einem Gebiet: Liste fuer die Detailansicht, nach Stufe sortiert
function Zones.Members(key)
local z = Zones.Get(key)
if not z then return {} end
local list = {}
local outSet = {}
for _, e in ipairs(z.out) do outSet[e] = true end
for _, e in ipairs(z.normal) do
list[#list + 1] = { id = e.id, l1 = e.l1, l2 = e.l2, w = e.w, out = outSet[e] or nil }
end
for _, e in ipairs(z.special) do
list[#list + 1] = { id = e.id, l1 = e.l1, l2 = e.l2, w = e.w, c = e.c }
end
table.sort(list, function(a, b)
if a.l1 ~= b.l1 then return a.l1 < b.l1 end
if a.l2 ~= b.l2 then return a.l2 < b.l2 end
return a.id < b.id
end)
return list
end
-- Passt die Stufe zum Gebiet? Rueckgabe: "in" (Stufe liegt in der Spanne), "below" (Spanne ist niedriger),
-- "above" (Spanne ist hoeher), nil (keine Daten). tol = Toleranz in Stufen.
function Zones.Compare(key, level, tol)
local z = Zones.Get(key)
if not z or not z.lo or z.few and not z.manual then return nil end
tol = tol or 0
if level < z.lo - tol then return "above" end
if level > z.hi + tol then return "below" end
return "in"
end
-- Gebiete, deren typische Spanne die Stufe enthaelt (mit Toleranz), mit genug Daten
function Zones.ForLevel(level, tol)
local out = {}
for key, z in pairs(Zones.All()) do
if Zones.Compare(key, level, tol) == "in" then out[#out + 1] = z end
end
return out
end