Dateien nach "/" hochladen

This commit is contained in:
2026-10-27 14:09:21 +00:00
parent dea535ff5c
commit 9147ef7737
4 changed files with 2238 additions and 0 deletions
+322
View File
@@ -0,0 +1,322 @@
-- Freewood-OctoWoW-Codex: Ortspunkte (Eingang zu Instanz oder Hoehle, Berg-Aufstieg, Engstelle, Zielpunkt)
--
-- Berechtigte (Modul Freewood-OctoWoW-Dev, Rolle Bearbeiter oder hoeher) erfassen einzelne Punkte, an denen der Levelplan vorbeimuss:
-- zum Beispiel der Eingang einer Instanz fuer eine Dungeon-Quest. Ein Punkt gehoert zu einer oder mehreren Quests (q) und/oder
-- zu einem ganzen Gebiet (am = Karte; negativ = Instanz). Gespeichert wird in db.spots[id]; das normale Teilen verschickt sie als
-- Datensatz "h<ID>", angenommen nur von Absendern, die selbst berechtigt sind.
--
-- db.spots[id] = { k = Art, f = "A" | "H" | nil, n = Name, p = { m, x, y }, q = { Quest-IDs }, am = Gebiet, del, upd }
-- Die ID ist ein Pruefwert aus Fraktion und Ort (auf etwa 0,5 % gerundet): wer an derselben Stelle erneut speichert, aendert den Punkt.
local ADDON, NS = ...
local FW = _G.Freewood
local Codex = NS and NS.Codex
if not FW or not Codex then
return
end
local Spots = { draft = { q = {} }, rev = 0 }
Codex.Spots = Spots
NS.Spots = Spots
Spots.KINDS = { "instance", "cave", "mountain", "pass", "point", "other" }
local KIND_OK = {}
for _, k in ipairs(Spots.KINDS) do KIND_OK[k] = true end
Spots.MAX_QUESTS = 20
local function Now() return _G.time and _G.time() or 0 end
local function DB()
local db = Codex.db
if not db then return nil end
db.spots = db.spots or {}
return db
end
local function CanEdit()
return Codex.Trust ~= nil and Codex.Trust.CanEdit()
end
local function Round(v, f) return math.floor(v * f + 0.5) end
local function Hash(text)
local h = 5381
for i = 1, #text do h = (h * 33 + text:byte(i)) % 99999989 end
return h
end
function Spots.Id(rec)
local p = rec.p or {}
return Hash(table.concat({ "s", tostring(rec.f or "-"), tostring(p.m), Round(p.x or 0, 200), Round(p.y or 0, 200) }, "|"))
end
-- Aenderung melden (auch fuer empfangene Datensaetze: ruft CodexShare auf)
function Spots.Bump(id)
Spots.rev = Spots.rev + 1
Spots.index = nil
FW:Fire("CODEX_SPOTS_CHANGED", id)
end
------------------------------------------------------------------------
-- Entwurf
------------------------------------------------------------------------
function Spots.Draft()
return Spots.draft
end
function Spots.Reset()
local d = Spots.draft
d.p, d.am, d.kind, d.faction, d.name, d.editing = nil, nil, nil, nil, nil, nil
d.q = {}
FW:Fire("CODEX_SPOTS_DRAFT")
end
-- Ort des Spielers { m, x, y } oder nil
function Spots.Here()
local T = Codex.Transport
if T and T.Here then return T.Here() end
return nil
end
function Spots.Capture()
local here = Spots.Here()
if not here then return false end
Spots.draft.p = here
FW:Fire("CODEX_SPOTS_DRAFT")
return true
end
-- Gebiet, in dem der Spieler gerade steht: Instanz (negativ) oder Karte
function Spots.AreaHere()
local inst = Codex.CurrentInstance and Codex.CurrentInstance()
if inst then return -inst end
local here = Spots.Here()
return here and here.m or nil
end
function Spots.SetArea(am)
Spots.draft.am = am
FW:Fire("CODEX_SPOTS_DRAFT")
end
function Spots.AddQuest(id)
id = tonumber(id)
local d = Spots.draft
if not id or id < 1 or id ~= math.floor(id) or #d.q >= Spots.MAX_QUESTS then return false end
for _, v in ipairs(d.q) do if v == id then return true end end
d.q[#d.q + 1] = id
FW:Fire("CODEX_SPOTS_DRAFT")
return true
end
function Spots.RemoveQuest(id)
local d = Spots.draft
for i, v in ipairs(d.q) do
if v == id then table.remove(d.q, i) FW:Fire("CODEX_SPOTS_DRAFT") return true end
end
return false
end
-- Quests zu einem Suchtext (Nummer oder Teil des Namens): { { id, title } }, hoechstens limit
function Spots.FindQuests(text, limit)
limit = limit or 8
local out = {}
text = tostring(text or ""):gsub("^%s+", ""):gsub("%s+$", "")
if text == "" then return out end
local num = tonumber(text)
local seed = Codex.Seed and Codex.Seed.Ready and Codex.Seed.Ready() and Codex.Seed or nil
local V = Codex.V and Codex.V()
local function Title(id)
return Codex.QuestTitle(V and V.quests[id], id)
end
if num then
out[1] = { id = num, title = Title(num) }
return out
end
local needle = text:lower()
local seen = {}
for id, q in pairs(V and V.quests or {}) do
local t = Title(id)
if t and t:lower():find(needle, 1, true) then
seen[id] = true
out[#out + 1] = { id = id, title = t }
end
end
if seed and seed.IDs then
for _, id in ipairs(seed.IDs("quests")) do
if not seen[id] and seed.Haystack("quests", id):find(needle, 1, true) then
out[#out + 1] = { id = id, title = Title(id) }
end
end
end
table.sort(out, function(a, b)
if a.title ~= b.title then return a.title < b.title end
return a.id < b.id
end)
while #out > limit do out[#out] = nil end
return out
end
-- Entwurf pruefen. Rueckgabe: true oder false, Grund
function Spots.CheckDraft()
local d = Spots.draft
if not d.p then return false, "pos" end
if not KIND_OK[d.kind or "instance"] then return false, "kind" end
if not d.am and #d.q == 0 then return false, "link" end
return true
end
------------------------------------------------------------------------
-- Speichern, Bearbeiten, Loeschen
------------------------------------------------------------------------
local function Touch(id)
if Codex.Share and Codex.Share.InvalidateScan then Codex.Share.InvalidateScan() end
Spots.Bump(id)
FW:Fire("CODEX_CHANGED", "spots", id)
end
local function Describe(rec)
local M = Codex.MapName
return string.format("%s: %s (%.1f / %.1f)%s", tostring(rec.k), M and M(rec.p.m) or rec.p.m, rec.p.x * 100, rec.p.y * 100,
rec.am and (" > " .. (M and M(rec.am) or rec.am)) or "")
end
local function Log(id, field, old, new)
if Codex.DevLog then
pcall(Codex.DevLog, { who = Codex.Trust and Codex.Trust.SelfKey() or "?", kind = "h", id = id, field = field, old = old, new = new })
end
end
-- Rueckgabe: id oder nil, Grund
function Spots.Save()
if not CanEdit() then return nil, "rights" end
local ok, why = Spots.CheckDraft()
if not ok then return nil, why end
local d = Spots.draft
local q = {}
for _, v in ipairs(d.q) do q[#q + 1] = v end
table.sort(q)
local name = type(d.name) == "string" and d.name:sub(1, 40) or ""
local rec = {
k = d.kind or "instance", f = (d.faction == "A" or d.faction == "H") and d.faction or nil,
n = name ~= "" and name or nil, p = { m = d.p.m, x = d.p.x, y = d.p.y }, q = q, am = d.am, upd = Now(),
}
local db = DB()
local id = Spots.Id(rec)
local old = db.spots[id]
rec.upd = math.max(rec.upd, (old and old.upd or 0) + 1)
db.spots[id] = rec
Log(id, "add", old and not old.del and Describe(old) or nil, Describe(rec))
Touch(id)
Spots.Reset()
return id
end
-- Liste { id, k, f, n, p, q, am, upd, del }; je ID der neueste Stand; geloeschte nur mit withDeleted
function Spots.All(withDeleted)
local best = {}
local db = DB()
if not db then return {} end
for id, rec in pairs(db.spots) do
if type(rec) == "table" and rec.p then best[id] = rec end
end
if Codex.Share and Codex.Share.RemoteRecords then
for _, rec in ipairs(Codex.Share.RemoteRecords("h")) do
local cur = best[rec.id]
if not cur or (rec.upd or 0) > (cur.upd or 0) then best[rec.id] = rec end
end
end
local out = {}
for id, rec in pairs(best) do
if withDeleted or not rec.del then
out[#out + 1] = { id = id, k = rec.k, f = rec.f, n = rec.n, p = rec.p, q = rec.q or {}, am = rec.am, upd = rec.upd, del = rec.del }
end
end
table.sort(out, function(x, y)
if x.p.m ~= y.p.m then return x.p.m < y.p.m end
return x.id < y.id
end)
return out
end
function Spots.Get(id)
for _, r in ipairs(Spots.All(true)) do
if r.id == id then return r end
end
return nil
end
-- Punkt in den Entwurf laden (zum Aendern); Speichern an derselben Stelle ersetzt ihn
function Spots.Edit(id)
local r = Spots.Get(id)
if not r or r.del then return false end
local d = Spots.draft
d.p = { m = r.p.m, x = r.p.x, y = r.p.y }
d.kind, d.faction, d.name, d.am, d.editing = r.k, r.f, r.n, r.am, id
d.q = {}
for _, v in ipairs(r.q) do d.q[#d.q + 1] = v end
FW:Fire("CODEX_SPOTS_DRAFT")
return true
end
function Spots.Delete(id)
if not CanEdit() then return false end
local rec = Spots.Get(id)
if not rec then return false end
local db = DB()
db.spots[id] = { k = rec.k, f = rec.f, n = rec.n, p = rec.p, q = rec.q, am = rec.am, del = true,
upd = math.max(Now(), (rec.upd or 0) + 1) }
Log(id, "del", Describe(rec), nil)
Touch(id)
return true
end
------------------------------------------------------------------------
-- Lesen fuer den Levelplan
------------------------------------------------------------------------
local EMPTY = {}
local function BuildIndex()
local idx = { quest = {}, area = {} }
for _, r in ipairs(Spots.All()) do
local s = { id = r.id, k = r.k, f = r.f, n = r.n, m = r.p.m, x = r.p.x, y = r.p.y }
for _, qid in ipairs(r.q) do
idx.quest[qid] = idx.quest[qid] or {}
table.insert(idx.quest[qid], s)
end
if r.am then
idx.area[r.am] = idx.area[r.am] or {}
table.insert(idx.area[r.am], s)
end
end
idx.empty = next(idx.quest) == nil and next(idx.area) == nil
return idx
end
-- Punkte, die fuer ein Ziel gelten: Quest-ID (oder nil) und Karte des Ziels (Instanzen negativ); faction = "A" | "H" | nil.
-- Rueckgabe: Liste { id, k, n, m, x, y } (leer, wenn nichts passt)
function Spots.For(questID, m, faction)
if not Spots.index then Spots.index = BuildIndex() end
local idx = Spots.index
if idx.empty then return EMPTY end
local out, seen = {}, {}
local function Add(list)
for _, s in ipairs(list or {}) do
if not seen[s.id] and not (faction and s.f and s.f ~= faction) then
seen[s.id] = true
out[#out + 1] = s
end
end
end
if questID then Add(idx.quest[questID]) end
if m then Add(idx.area[m]) end
return out
end
function Spots.Count()
return #Spots.All()
end
+1262
View File
File diff suppressed because it is too large Load Diff
+505
View File
@@ -0,0 +1,505 @@
-- Freewood-OctoWoW-Codex: Flugrouten (Taxi)
--
-- Erfasst beim Flug: Start- und Zielpunkt, die Strecke (Punkte alle paar Sekunden), die Flugdauer und den bezahlten Preis.
-- Je Charakter: welche Flugpunkte er kennt, wie viel Gold er fuer Fluege ausgegeben hat und wie oft er geflogen ist.
--
-- db.taxi.nodes[id] = { n = Name, w = Welt, tx, ty = Position auf der Flugkarte, a, b = Weltposition (erster, zweiter Wert von UnitPosition) }
-- db.taxi.routes[id] = { a = Start-ID, b = Ziel-ID, t = Sekunden, c = Preis in Kupfer, n = Anzahl, p = { a1, b1, a2, b2, ... }, upd = Stand }
-- db.taxiChar[Charakter] = { known = { [id] = true }, spent = Kupfer, trips = Anzahl, log = { { t, a, b, c, d }, ... } }
-- Die Flugpunkt-ID kommt aus der Position auf der Flugkarte (sprachunabhaengig), sonst aus dem Namen.
-- Zwischenlandungen: aufgezeichnet wird die Gesamtstrecke bis zum gewaehlten Ziel.
local ADDON, NS = ...
local FW = _G.Freewood
local Codex = NS and NS.Codex
if not FW or not Codex then
return
end
local Taxi = { state = {} }
Codex.Taxi = Taxi
NS.Taxi = Taxi
local state = Taxi.state
local SAMPLE = 3 -- Sekunden zwischen zwei Streckenpunkten
local MAX_POINTS = 120 -- Punkte je Route (je 2 Zahlen)
local MAX_ROUTES = 600
local MAX_NODES = 300
local MAX_LOG = 30
local PRICE_WINDOW = 6 -- Sekunden nach dem Start, in denen der abgezogene Betrag nachgemessen wird
local function Clock() return _G.GetTime and _G.GetTime() or 0 end
local function Now() return _G.time and _G.time() or 0 end
local function Round(v) return math.floor((v or 0) + 0.5) end
local function DB()
local db = Codex.db
if not db then return nil end
local t = db.taxi
if type(t) ~= "table" then t = {}; db.taxi = t end
t.nodes = t.nodes or {}
t.routes = t.routes or {}
return t
end
local function CharRec(create)
local db = Codex.db
local key = Codex.Trust and Codex.Trust.SelfKey and Codex.Trust.SelfKey() or nil
if not db or not key then return nil end
db.taxiChar = db.taxiChar or {}
local r = db.taxiChar[key]
if not r and create ~= false then
r = { known = {}, spent = 0, trips = 0, log = {} }
db.taxiChar[key] = r
end
return r
end
function Taxi.Money(c)
c = math.floor(tonumber(c) or 0)
local g, s, k = math.floor(c / 10000), math.floor(c / 100) % 100, c % 100
local out = {}
if g > 0 then out[#out + 1] = g .. "g" end
if s > 0 then out[#out + 1] = s .. "s" end
if k > 0 or #out == 0 then out[#out + 1] = k .. "c" end
return table.concat(out, " ")
end
function Taxi.Duration(sec)
sec = math.floor(tonumber(sec) or 0)
return string.format("%d:%02d", math.floor(sec / 60), sec % 60)
end
------------------------------------------------------------------------
-- Abfragen (fuer Karte und Planer)
------------------------------------------------------------------------
Taxi.rev = 0
local merged
-- Eigene und empfangene Routen zusammen (eigene haben Vorrang)
function Taxi.Routes()
local t = DB()
if not t then return {} end
local rem = Codex.db.taxir
if not rem or next(rem) == nil then return t.routes end
if merged and merged.rev == Taxi.rev then return merged.routes end
local out = {}
for k, r in pairs(rem) do out[k] = r end
for k, r in pairs(t.routes) do out[k] = r end
merged = { rev = Taxi.rev, routes = out }
return out
end
function Taxi.Node(id)
local t = DB()
if not t then return nil end
local n = t.nodes[id]
if n then return n end
local rn = Codex.db.taxin
return rn and rn[id] or nil
end
-- Kennt dieser Charakter den Flugpunkt?
function Taxi.Known(id)
local c = CharRec(false)
return c and c.known and c.known[id] and true or false
end
function Taxi.CharStats()
local c = CharRec(false)
if not c then return nil end
local n = 0
for _ in pairs(c.known or {}) do n = n + 1 end
return { known = n, spent = c.spent or 0, trips = c.trips or 0, secs = c.secs or 0, log = c.log, routes = c.rt }
end
-- Wie oft dieser Charakter die Route geflogen ist und wie lange insgesamt: n, Sekunden
function Taxi.RouteStat(a, b)
local c = CharRec(false)
local r = c and c.rt and c.rt[a .. ">" .. b]
if not r then return 0, 0 end
return r.n or 0, r.sec or 0
end
------------------------------------------------------------------------
-- Flugkarte lesen
------------------------------------------------------------------------
local function NodeId(i, world)
if _G.TaxiNodePosition then
local ok, x, y = pcall(_G.TaxiNodePosition, i)
if ok and type(x) == "number" and type(y) == "number" then
return string.format("%d:%d:%d", world or 0, Round(x * 1000), Round(y * 1000)), x, y
end
end
local name = _G.TaxiNodeName and _G.TaxiNodeName(i)
if type(name) == "string" and name ~= "" then return "n:" .. name, nil, nil end
return nil
end
-- Kartenposition des Flugpunkts (Karte, x, y), solange der Spieler dort steht; fuer den Levelplaner
local function SetMapPos(n)
local loc = FW.Util and FW.Util.GetPlayerLocation and FW.Util.GetPlayerLocation()
if loc and loc.mapID and loc.x and loc.y and loc.x > 0 and loc.y > 0 then
if n.m ~= loc.mapID or not n.x then
n.m, n.x, n.y = loc.mapID, loc.x, loc.y
return true
end
end
return false
end
function Taxi.Scan()
local count = _G.NumTaxiNodes and _G.NumTaxiNodes() or 0
if type(count) ~= "number" or count <= 0 then return end
local T, C = DB(), CharRec(true)
if not T or not C then return end
local P = Codex.Path and Codex.Path.Read and Codex.Path.Read()
local world = P and P.world or 0
local nodes = 0
for _ in pairs(T.nodes) do nodes = nodes + 1 end
state.slot, state.cur = {}, nil
local changed = false
for i = 1, math.min(count, 120) do
local kind = _G.TaxiNodeGetType and _G.TaxiNodeGetType(i)
local name = _G.TaxiNodeName and _G.TaxiNodeName(i)
local id, tx, ty = NodeId(i, world)
if id then
local n = T.nodes[id]
if not n and nodes < MAX_NODES then
n = {}
T.nodes[id] = n
nodes = nodes + 1
end
if n then
if type(name) == "string" and name ~= "" then n.n = name:sub(1, 60) end
n.w, n.tx, n.ty = world, tx, ty
end
if kind ~= "NONE" and not C.known[id] then C.known[id] = true; changed = true end
if kind == "CURRENT" then
state.cur = id
if n and P then n.a, n.b = Round(P.y), Round(P.x) end
if n and SetMapPos(n) then changed = true end
end
local cost = _G.TaxiNodeCost and tonumber(_G.TaxiNodeCost(i)) or nil
state.slot[i] = { id = id, cost = cost and math.floor(cost) or nil, kind = kind }
end
end
if changed then
Taxi.rev = Taxi.rev + 1
FW:Fire("CODEX_TAXI_CHANGED")
end
if Taxi.OnOpen then pcall(Taxi.OnOpen) end
end
------------------------------------------------------------------------
-- Teilen (Share/Sync, Datensatzart "f", je Route ein Datensatz)
------------------------------------------------------------------------
-- Zahl zur Route: Flugpunkt-Zahl (Welt, x, y) * 10^7 + Flugpunkt-Zahl des Ziels. Nur Flugpunkte mit Positions-ID sind teilbar.
local NODE_W = 1002001
local SHARE_POINTS = 40
local function NodeNum(id)
local w, x, y = tostring(id):match("^(%d+):(%d+):(%d+)$")
w, x, y = tonumber(w), tonumber(x), tonumber(y)
if not w or w > 8 or x > 1000 or y > 1000 then return nil end
return w * NODE_W + x * 1001 + y
end
local function NodeFromNum(n)
if type(n) ~= "number" or n < 0 or n >= 9 * NODE_W then return nil end
local w, r = math.floor(n / NODE_W), n % NODE_W
return string.format("%d:%d:%d", w, math.floor(r / 1001), r % 1001)
end
local function RouteNum(a, b)
local na, nb = NodeNum(a), NodeNum(b)
if not na or not nb then return nil end
return na * 10000000 + nb
end
local function RouteIds(num)
local a, b = NodeFromNum(math.floor(num / 10000000)), NodeFromNum(num % 10000000)
if not a or not b then return nil end
return a, b
end
function Taxi.SyncList()
local out = {}
local t = DB()
if not t then return out end
for _, r in pairs(t.routes) do
local num = r.upd and RouteNum(r.a, r.b)
if num then out[#out + 1] = { key = "f" .. string.format("%.0f", num), upd = r.upd } end
end
return out
end
local function NodeShare(id)
local n = Taxi.Node(id) or {}
return { n = n.n, a = n.a, b = n.b, m = n.m, x = n.x, y = n.y }
end
-- Eigene Route als Datensatz (Strecke auf hoechstens SHARE_POINTS Punkte verkleinert)
function Taxi.RouteRecord(num)
local a, b = RouteIds(num)
local t = DB()
local r = a and t and t.routes[a .. ">" .. b]
if not r or not r.upd or type(r.p) ~= "table" then return nil end
local pts, m = {}, math.floor(#r.p / 2)
if m < 2 then return nil end
local take = math.min(m, SHARE_POINTS)
for i = 1, take do
local j = (take == 1) and 1 or (1 + math.floor((i - 1) * (m - 1) / (take - 1) + 0.5))
pts[#pts + 1] = r.p[j * 2 - 1]
pts[#pts + 1] = r.p[j * 2]
end
return { t = r.t, c = r.c, n = r.n or 1, p = pts, A = NodeShare(a), B = NodeShare(b) }, r.upd
end
local function NodeFromRecord(id, rn)
local w, x, y = tostring(id):match("^(%d+):(%d+):(%d+)$")
return { n = rn.n, w = tonumber(w), tx = tonumber(x) / 1000, ty = tonumber(y) / 1000, a = rn.a, b = rn.b, m = rn.m, x = rn.x, y = rn.y }
end
-- Empfangene Route einmischen. Rueckgabe: true, wenn gespeichert
function Taxi.StoreRemote(rec)
local db = Codex.db
if not db or type(rec) ~= "table" then return false end
local a, b = RouteIds(rec.id)
if not a then return false end
db.taxir = db.taxir or {}
db.taxin = db.taxin or {}
local rid = a .. ">" .. b
local old = db.taxir[rid]
if old and (old.upd or 0) >= rec.upd then return false end
if not old then
local count = 0
for _ in pairs(db.taxir) do count = count + 1 end
if count >= MAX_ROUTES then return false end
end
db.taxir[rid] = { a = a, b = b, t = rec.t, c = rec.c, n = rec.n, p = rec.p, upd = rec.upd }
local own = DB()
for id, rn in pairs({ [a] = rec.A, [b] = rec.B }) do
if not (own and own.nodes[id]) and not db.taxin[id] then
local nodes = 0
for _ in pairs(db.taxin) do nodes = nodes + 1 end
if nodes < MAX_NODES then db.taxin[id] = NodeFromRecord(id, rn) end
end
end
Taxi.rev = Taxi.rev + 1
return true
end
-- Levelplaner: Fluege als Kanten { id, k, t, wait, from = { m, x, y }, to = { m, x, y } }, nur zwischen Flugpunkten,
-- die dieser Charakter kennt und deren Kartenposition bekannt ist
function Taxi.PlannerOn()
local db = Codex.db
return not (db and db.settings and db.settings.taxiPlan == false)
end
local edgeCache
function Taxi.Edges()
if not Taxi.PlannerOn() then return {} end
local key = Codex.Trust and Codex.Trust.SelfKey and Codex.Trust.SelfKey() or "?"
if edgeCache and edgeCache.rev == Taxi.rev and edgeCache.key == key then return edgeCache.list end
local list = {}
for rid, r in pairs(Taxi.Routes()) do
local na, nb = Taxi.Node(r.a), Taxi.Node(r.b)
if na and nb and na.m and nb.m and r.t and Taxi.Known(r.a) and Taxi.Known(r.b) then
list[#list + 1] = { id = "f" .. rid, k = "flight", t = r.t, wait = 20, name = nb.n, fa = r.a, fb = r.b,
from = { m = na.m, x = na.x, y = na.y }, to = { m = nb.m, x = nb.x, y = nb.y } }
end
end
edgeCache = { rev = Taxi.rev, key = key, list = list }
return list
end
function Taxi.ClearRemote()
local db = Codex.db
if db then db.taxir, db.taxin = nil, nil end
Taxi.rev = Taxi.rev + 1
end
------------------------------------------------------------------------
-- Flug erfassen
------------------------------------------------------------------------
local function OnTake(slot)
local s = state.slot and state.slot[slot]
if not s or not state.cur then return end
state.pending = { from = state.cur, to = s.id, quote = s.cost or 0, money0 = _G.GetMoney and _G.GetMoney() or 0,
asked = Clock(), started = false }
end
local function Finish(p, now)
state.pending = nil
Taxi.flight = nil
local T, C = DB(), CharRec(true)
if not T or not C then return end
local dur = now - p.t0
if dur < 5 then return end
-- Endpunkt: Position der Landung
local P = Codex.Path and Codex.Path.Read and Codex.Path.Read()
if P then
p.pts[#p.pts + 1] = Round(P.y)
p.pts[#p.pts + 1] = Round(P.x)
local n = T.nodes[p.to]
if n and not n.a then n.a, n.b = Round(P.y), Round(P.x) end
if n and not n.m then SetMapPos(n) end
end
local paid = p.paid or 0
if paid <= 0 or paid > p.quote * 3 + 100 then paid = p.quote or 0 end
local rid = p.from .. ">" .. p.to
local r = T.routes[rid]
local isNew = false
if not r then
local count = 0
for _ in pairs(T.routes) do count = count + 1 end
if count >= MAX_ROUTES then return end
r = { a = p.from, b = p.to, t = Round(dur), c = paid, n = 1, p = p.pts, upd = Now() }
T.routes[rid] = r
isNew = true
else
r.n = (r.n or 1) + 1
r.t = Round((r.t * (r.n - 1) + dur) / r.n)
r.c = paid
if #p.pts > #(r.p or {}) then r.p = p.pts end
r.upd = Now()
end
C.known[p.to] = true
C.spent = (C.spent or 0) + paid
C.trips = (C.trips or 0) + 1
C.secs = (C.secs or 0) + Round(dur)
C.rt = C.rt or {}
local rs = C.rt[rid]
if not rs then rs = { n = 0, sec = 0, gold = 0 }; C.rt[rid] = rs end
rs.n, rs.sec, rs.gold = rs.n + 1, rs.sec + Round(dur), (rs.gold or 0) + paid
C.log = C.log or {}
C.log[#C.log + 1] = { t = Now(), a = p.from, b = p.to, c = paid, d = Round(dur) }
while #C.log > MAX_LOG do table.remove(C.log, 1) end
if isNew then
local na, nb = T.nodes[p.from], T.nodes[p.to]
FW:Print(string.format("Codex: Flugroute %s -> %s gespeichert (%s, %s, %d Punkte)",
na and na.n or p.from, nb and nb.n or p.to, Taxi.Duration(dur), Taxi.Money(paid), math.floor(#p.pts / 2)))
end
Taxi.rev = Taxi.rev + 1
FW:Fire("CODEX_TAXI_CHANGED")
FW:Fire("CODEX_CHANGED", "taxi")
end
local function Tick()
local p = state.pending
if not p then return end
local now = Clock()
local on = _G.UnitOnTaxi and _G.UnitOnTaxi("player") and true or false
if on and not p.started then
p.started, p.t0, p.nextAt, p.pts = true, now, now, {}
local r = Taxi.Routes()[p.from .. ">" .. p.to]
local nd = Taxi.Node(p.to)
Taxi.flight = { t0 = now, exp = r and r.t or nil, name = nd and nd.n or "?" }
end
if p.started then
if on then
if now - p.t0 <= PRICE_WINDOW then
local d = p.money0 - (_G.GetMoney and _G.GetMoney() or 0)
if d > (p.paid or 0) then p.paid = d end
end
if now >= p.nextAt and #p.pts < MAX_POINTS * 2 then
local P = Codex.Path and Codex.Path.Read and Codex.Path.Read()
if P then
p.pts[#p.pts + 1] = Round(P.y)
p.pts[#p.pts + 1] = Round(P.x)
end
p.nextAt = now + SAMPLE
end
else
Finish(p, now)
end
elseif now - p.asked > 15 then
state.pending = nil -- Flug nicht angetreten
Taxi.flight = nil
end
end
local acc = 0
local frame = _G.CreateFrame and _G.CreateFrame("Frame")
if frame then
frame:SetScript("OnUpdate", function(_, elapsed)
acc = acc + (elapsed or 0)
if acc < 1 then return end
acc = 0
pcall(Tick)
end)
end
-- Tooltip am Flugziel auf der Flugkarte: Reisedauer (und wie oft schon geflogen)
function Taxi.ButtonTip(button)
if not (button and button.GetID and state.cur and state.slot) then return end
local s = state.slot[button:GetID()]
local tt = _G.GameTooltip
if not s or not tt or s.id == state.cur then return end
local r = Taxi.Routes()[state.cur .. ">" .. s.id]
local L = FW.L
if r and r.t then
tt:AddLine(string.format(L.CODEX_TAXI_TIP_TIME, Taxi.Duration(r.t)), 0.94, 0.75, 0.36)
else
tt:AddLine(L.CODEX_TAXI_TIP_NONE, 0.6, 0.59, 0.55)
end
local n, sec = Taxi.RouteStat(state.cur, s.id)
if n > 0 then
tt:AddLine(string.format(L.CODEX_TAXI_TIP_COUNT, n, Taxi.Duration(sec)), 0.6, 0.59, 0.55)
end
tt:Show()
end
local function Install()
if Taxi.hooked then return end
Taxi.hooked = true
if _G.hooksecurefunc and _G.TaxiNodeOnButtonEnter then
pcall(_G.hooksecurefunc, "TaxiNodeOnButtonEnter", function(button) pcall(Taxi.ButtonTip, button) end)
end
if _G.hooksecurefunc and _G.TakeTaxiNode then
pcall(_G.hooksecurefunc, "TakeTaxiNode", function(slot) pcall(OnTake, slot) end)
end
FW:RegisterEvent("TAXIMAP_OPENED", function() pcall(Taxi.Scan) end, Taxi)
end
Install()
------------------------------------------------------------------------
-- /fwtaxi
------------------------------------------------------------------------
SLASH_FREEWOODTAXI1 = "/fwtaxi"
SlashCmdList["FREEWOODTAXI"] = function(arg)
local T = DB()
if not T then return end
local nodes, routes = 0, 0
for _ in pairs(T.nodes) do nodes = nodes + 1 end
for _ in pairs(T.routes) do routes = routes + 1 end
if arg == "diag" then
FW:Print(string.format("Taxi: TakeTaxiNode=%s TaxiNodePosition=%s TaxiNodeCost=%s TaxiNodeGetType=%s UnitOnTaxi=%s Hook=%s",
tostring(_G.TakeTaxiNode ~= nil), tostring(_G.TaxiNodePosition ~= nil), tostring(_G.TaxiNodeCost ~= nil),
tostring(_G.TaxiNodeGetType ~= nil), tostring(_G.UnitOnTaxi ~= nil), tostring(Taxi.hooked)))
FW:Print(string.format("Taxi: aktueller Punkt=%s, Plaetze=%d, Flug laeuft=%s", tostring(state.cur), state.slot and #state.slot or 0,
tostring(state.pending and state.pending.started or false)))
return
end
local c = Taxi.CharStats()
FW:Print(string.format("Taxi: %d Flugpunkte, %d Routen gespeichert.", nodes, routes))
if c then
FW:Print(string.format("Taxi: dieser Charakter kennt %d Flugpunkte, %d Fluege, %s ausgegeben, %s in der Luft.", c.known, c.trips, Taxi.Money(c.spent), Taxi.Duration(c.secs)))
end
local list = {}
for _, r in pairs(T.routes) do list[#list + 1] = r end
table.sort(list, function(x, y) return (x.upd or 0) > (y.upd or 0) end)
for i = 1, math.min(#list, 5) do
local r = list[i]
local na, nb = T.nodes[r.a], T.nodes[r.b]
FW:Print(string.format(" %s -> %s: %s, %s, %d Punkte, %dx", na and na.n or r.a, nb and nb.n or r.b, Taxi.Duration(r.t), Taxi.Money(r.c),
math.floor(#(r.p or {}) / 2), r.n or 1))
end
end
+149
View File
@@ -0,0 +1,149 @@
-- Freewood-OctoWoW-Codex: Flugfenster
--
-- Waehrend eines Fluges mit dem Flugmeister: kleines Fenster mit Zielname, verbleibender Zeit und einem ablaufenden Balken.
-- Ist die Flugdauer der Route noch unbekannt (noch nie geflogen oder empfangen), zaehlt es die vergangene Zeit hoch.
local ADDON, NS = ...
local FW = _G.Freewood
local Codex = NS and NS.Codex
local Taxi = NS and NS.Taxi
if not FW or not Codex or not Taxi or not FW.UI then
return
end
local UI = FW.UI
local WIDTH, HEIGHT, BAR_H = 250, 50, 8
local win
local function Settings()
local db = Codex.db
if not db then return {} end
db.settings = db.settings or {}
return db.settings
end
function Taxi.WindowOn()
return Settings().taxiWindow ~= false
end
local function SavePos()
if not win then return end
local point, _, rel, x, y = win:GetPoint(1)
if point then Settings().taxiWinPos = { point = point, rel = rel, x = x, y = y } end
end
local function ApplyPos()
win:ClearAllPoints()
local p = Settings().taxiWinPos
if type(p) == "table" and p.point and p.x and p.y then
win:SetPoint(p.point, UIParent, p.rel or p.point, p.x, p.y)
else
win:SetPoint("TOP", UIParent, "TOP", 0, -150)
end
end
local function Build()
local f = UI.CreatePanel(UIParent, "bg", "gold")
f:SetSize(WIDTH, HEIGHT)
f:SetFrameStrata("MEDIUM")
f:SetClampedToScreen(true)
f:SetMovable(true)
f:EnableMouse(true)
f:RegisterForDrag("LeftButton")
f:SetScript("OnDragStart", function(self) self:StartMoving() end)
f:SetScript("OnDragStop", function(self) self:StopMovingOrSizing(); SavePos() end)
win = f
ApplyPos()
f.title = UI.CreateLabel(f, "", { size = 12, color = "goldLight" })
f.title:SetPoint("TOPLEFT", f, "TOPLEFT", 10, -7)
f.title:SetPoint("TOPRIGHT", f, "TOPRIGHT", -10, -7)
f.title:SetHeight(14)
f.time = UI.CreateLabel(f, "", { size = 11, color = "text" })
f.time:SetPoint("TOPLEFT", f.title, "BOTTOMLEFT", 0, -2)
f.time:SetPoint("TOPRIGHT", f.title, "BOTTOMRIGHT", 0, -2)
f.time:SetHeight(13)
f.barBg = f:CreateTexture(nil, "ARTWORK")
f.barBg:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 10, 8)
f.barBg:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -10, 8)
f.barBg:SetHeight(BAR_H)
f.barBg:SetColorTexture(UI.Color("card"))
f.fill = f:CreateTexture(nil, "OVERLAY")
f.fill:SetPoint("BOTTOMLEFT", f.barBg, "BOTTOMLEFT", 0, 0)
f.fill:SetHeight(BAR_H)
f.fill:SetColorTexture(UI.Color("gold"))
f.acc = 0
f:SetScript("OnUpdate", function(self, elapsed)
self.acc = self.acc + (elapsed or 0)
if self.acc < 0.1 then return end
self.acc = 0
local fl = Taxi.flight
if not fl or not Taxi.WindowOn() then self:Hide() return end
local L = FW.L
local el = (GetTime and GetTime() or 0) - fl.t0
if self.shownName ~= fl.name then
self.shownName = fl.name
self.title:SetText(string.format(L.CODEX_TAXI_TITLE, fl.name))
end
local full = self.barBg:GetWidth() or (WIDTH - 20)
if fl.exp and fl.exp > 0 then
local left = fl.exp - el
if left > 0 then
self.time:SetText(string.format(L.CODEX_TAXI_LEFT, Taxi.Duration(math.ceil(left))))
self.fill:SetColorTexture(UI.Color("gold"))
self.fill:SetWidth(math.max(1, full * left / fl.exp))
else
self.time:SetText(L.CODEX_TAXI_ARRIVING)
self.fill:SetColorTexture(UI.Color("success"))
self.fill:SetWidth(1)
end
else
self.time:SetText(string.format(L.CODEX_TAXI_UNKNOWN, Taxi.Duration(el)))
self.fill:SetColorTexture(UI.Color("bronze"))
self.fill:SetWidth(math.max(1, full * ((el % 4) / 4)))
end
end)
f:Hide()
return f
end
-- Das Fenster zeigt sich, sobald ein Flug laeuft
local watch = CreateFrame("Frame")
local acc = 0
watch:SetScript("OnUpdate", function(_, elapsed)
acc = acc + (elapsed or 0)
if acc < 0.25 then return end
acc = 0
if Taxi.flight and Taxi.WindowOn() then
if not win then win = Build() end
if not win:IsShown() then win.shownName = nil; win:Show() end
end
end)
if UI.RegisterSettingsSection then
UI:RegisterSettingsSection("codex-taxi", {
order = 108,
height = 140,
build = function(parent)
local L = FW.L
local f, stack = UI.CreateSection(parent, L.CODEX_TAXI_HEADER)
local note = UI.CreateLabel(f, L.CODEX_TAXI_NOTE, { size = 11, color = "textDim", justifyV = "TOP" })
stack:Add(note, { fit = true, gap = 8 })
local cb = UI.CreateCheckbox(f, L.CODEX_TAXI_SWITCH, function() return Taxi.WindowOn() end,
function(on) Settings().taxiWindow = on and true or false end)
stack:Add(cb, { fit = true, gap = 6 })
local cp = UI.CreateCheckbox(f, L.CODEX_TAXI_PLAN, function() return Taxi.PlannerOn() end,
function(on) Settings().taxiPlan = on and true or false end)
stack:Add(cp, { fit = true, gap = 6 })
function f.Refresh() cb:Refresh(); cp:Refresh(); stack:Layout() end
stack:Layout()
return f
end,
refresh = function(frame) frame.Refresh() end,
})
end