Ajouter main.lua
This commit is contained in:
309
main.lua
Normal file
309
main.lua
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
-- leaderboard.lua
|
||||||
|
-- Fetches player stats and displays a cycling leaderboard on wrapped monitors.
|
||||||
|
|
||||||
|
local API_URL = "https://script.lclr.dev/api/w/scripts/jobs/run_wait_result/p/f/scripts/cc-stats"
|
||||||
|
local API_TOKEN = "4mHiIDc5y1YBX05j68MFyhVoo2qbICe3"
|
||||||
|
local CYCLE_DELAY = 5 -- seconds per stat
|
||||||
|
local FETCH_COOLDOWN = 300 -- seconds between fetches (5 minutes)
|
||||||
|
|
||||||
|
-- Stats to cycle through (in order)
|
||||||
|
local STAT_KEYS = {
|
||||||
|
"playtime",
|
||||||
|
"deaths",
|
||||||
|
"mobs_killed",
|
||||||
|
"damage_taken",
|
||||||
|
"damage_dealt",
|
||||||
|
"distance_walked",
|
||||||
|
"lootr",
|
||||||
|
"waystones",
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Rank colours (ComputerCraft colour constants)
|
||||||
|
local RANK_COLORS = {
|
||||||
|
[1] = colors.yellow, -- gold
|
||||||
|
[2] = colors.lightGray,
|
||||||
|
[3] = colors.orange, -- brown-ish (closest CC colour)
|
||||||
|
}
|
||||||
|
local DEFAULT_COLOR = colors.white
|
||||||
|
local TITLE_COLOR = colors.cyan
|
||||||
|
local SEP_COLOR = colors.gray
|
||||||
|
local LABEL_COLOR = colors.lightBlue
|
||||||
|
local BG_COLOR = colors.black
|
||||||
|
|
||||||
|
-- Helpers
|
||||||
|
|
||||||
|
local function getMonitors()
|
||||||
|
local mons = {}
|
||||||
|
for _, name in ipairs(peripheral.getNames()) do
|
||||||
|
if peripheral.getType(name) == "monitor" then
|
||||||
|
local m = peripheral.wrap(name)
|
||||||
|
if m then
|
||||||
|
m.setTextScale(0.5) -- start at base scale; will be overridden
|
||||||
|
table.insert(mons, m)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return mons
|
||||||
|
end
|
||||||
|
|
||||||
|
local function centerText(mon, y, text, color, scale)
|
||||||
|
-- scale is unused here; CC monitors don't support per-text scaling,
|
||||||
|
-- so we use setTextScale on the whole monitor between draws.
|
||||||
|
local w, _ = mon.getSize()
|
||||||
|
local x = math.max(1, math.floor((w - #text) / 2) + 1)
|
||||||
|
mon.setCursorPos(x, y)
|
||||||
|
if color then mon.setTextColor(color) end
|
||||||
|
mon.write(text)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function leftText(mon, y, text, color)
|
||||||
|
mon.setCursorPos(1, y)
|
||||||
|
if color then mon.setTextColor(color) end
|
||||||
|
mon.write(text)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function truncate(str, maxLen)
|
||||||
|
if #str > maxLen then
|
||||||
|
return str:sub(1, maxLen - 1) .. "."
|
||||||
|
end
|
||||||
|
return str
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Data fetching
|
||||||
|
|
||||||
|
local function fetchData()
|
||||||
|
local ok, result = pcall(function()
|
||||||
|
-- Build JSON payload with list of stats to fetch
|
||||||
|
local payload = textutils.serialiseJSON(STAT_KEYS)
|
||||||
|
|
||||||
|
local resp = http.post(
|
||||||
|
API_URL,
|
||||||
|
payload,
|
||||||
|
{ ["Content-Type"] = "application/json", ["Authorization"] = "Bearer " .. API_TOKEN }
|
||||||
|
)
|
||||||
|
|
||||||
|
if not resp then error("HTTP request failed") end
|
||||||
|
local raw = resp.readAll()
|
||||||
|
resp.close()
|
||||||
|
return textutils.unserialiseJSON(raw)
|
||||||
|
end)
|
||||||
|
if ok and type(result) == "table" then
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Build sorted list of {username, value, type, label} for a given stat key.
|
||||||
|
-- New format: {"<pseudo>":{"<stat_name>":{"type":"<type>","data":"<value>","label":"<label>"}}}
|
||||||
|
-- Skips players with no value for the stat. Returns nil if list is empty.
|
||||||
|
local function buildLeaderboard(data, statKey)
|
||||||
|
local entries = {}
|
||||||
|
local statLabel = statKey -- fallback if no label found
|
||||||
|
|
||||||
|
for username, stats in pairs(data) do
|
||||||
|
if type(stats) == "table" and stats[statKey] then
|
||||||
|
local statInfo = stats[statKey]
|
||||||
|
if type(statInfo) == "table" and statInfo.data then
|
||||||
|
-- Extract label from first valid entry
|
||||||
|
if statInfo.label and statLabel == statKey then
|
||||||
|
statLabel = statInfo.label
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Convert data to number
|
||||||
|
local val = tonumber(statInfo.data)
|
||||||
|
if val then
|
||||||
|
table.insert(entries, {
|
||||||
|
username = username,
|
||||||
|
value = val,
|
||||||
|
dataType = statInfo.type or "int"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #entries == 0 then return nil end
|
||||||
|
table.sort(entries, function(a, b) return a.value > b.value end)
|
||||||
|
return entries, statLabel
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Format a raw value into a readable string based on type
|
||||||
|
local function formatValue(dataType, val)
|
||||||
|
if dataType == "time" then
|
||||||
|
-- Assuming time is in ticks (20 ticks/s)
|
||||||
|
local secs = math.floor(val / 20)
|
||||||
|
local mins = math.floor(secs / 60)
|
||||||
|
local hours = math.floor(mins / 60)
|
||||||
|
mins = mins % 60
|
||||||
|
return string.format("%dh %02dm", hours, mins)
|
||||||
|
elseif dataType == "distance" then
|
||||||
|
-- Distance in centimeters
|
||||||
|
local m = math.floor(val / 100)
|
||||||
|
if m >= 1000 then
|
||||||
|
return string.format("%.1f km", m / 1000)
|
||||||
|
end
|
||||||
|
return m .. " m"
|
||||||
|
else
|
||||||
|
-- Format large numbers with k/m suffix
|
||||||
|
if val >= 1000000 then
|
||||||
|
return string.format("%.1fm", val / 1000000)
|
||||||
|
elseif val >= 1000 then
|
||||||
|
return string.format("%.1fk", val / 1000)
|
||||||
|
end
|
||||||
|
return tostring(math.floor(val))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Rendering
|
||||||
|
|
||||||
|
local function renderLeaderboard(mon, entries, statLabel)
|
||||||
|
mon.setBackgroundColor(BG_COLOR)
|
||||||
|
mon.clear()
|
||||||
|
mon.setTextColor(DEFAULT_COLOR)
|
||||||
|
|
||||||
|
local w, h = mon.getSize()
|
||||||
|
local sep = string.rep("-", w)
|
||||||
|
|
||||||
|
-- Section 1: Title (large)
|
||||||
|
mon.setTextScale(1.5)
|
||||||
|
w, h = mon.getSize() -- dimensions change with text scale
|
||||||
|
|
||||||
|
-- Row 1: "CLASSEMENT"
|
||||||
|
centerText(mon, 1, "CLASSEMENT", TITLE_COLOR)
|
||||||
|
|
||||||
|
-- Section 2: Separator + stat label (medium)
|
||||||
|
mon.setTextScale(1)
|
||||||
|
w, h = mon.getSize()
|
||||||
|
sep = string.rep("-", w)
|
||||||
|
|
||||||
|
-- We've already drawn row 1 at scale 1.5; now we're at scale 1.
|
||||||
|
-- CC resets after setTextScale, so we redraw everything at one scale.
|
||||||
|
-- Strategy: do two passes — draw title zone, then switch scale for rest.
|
||||||
|
-- Simpler approach: use a fixed scale and use padding rows instead.
|
||||||
|
-- Re-draw everything at uniform scale 1 so layout is predictable.
|
||||||
|
|
||||||
|
mon.setTextScale(1)
|
||||||
|
w, h = mon.getSize()
|
||||||
|
mon.setBackgroundColor(BG_COLOR)
|
||||||
|
mon.clear()
|
||||||
|
sep = string.rep("-", w)
|
||||||
|
|
||||||
|
local row = 1
|
||||||
|
|
||||||
|
-- Title
|
||||||
|
centerText(mon, row, "CLASSEMENT", TITLE_COLOR) ; row = row + 1
|
||||||
|
-- Separator
|
||||||
|
mon.setCursorPos(1, row) ; mon.setTextColor(SEP_COLOR) ; mon.write(sep) ; row = row + 1
|
||||||
|
-- Stat label (from API)
|
||||||
|
centerText(mon, row, statLabel, LABEL_COLOR) ; row = row + 1
|
||||||
|
-- Separator
|
||||||
|
mon.setCursorPos(1, row) ; mon.setTextColor(SEP_COLOR) ; mon.write(sep) ; row = row + 1
|
||||||
|
|
||||||
|
-- Players
|
||||||
|
-- rank 1 gets a "large" look: prefix + name on one line, value below,
|
||||||
|
-- with some visual weight. Since CC doesn't support mid-render font
|
||||||
|
-- scaling, we approximate:
|
||||||
|
-- rank 1 -> full width, capitalised name, value right-aligned
|
||||||
|
-- ranks 2-3 -> indented slightly, normal case
|
||||||
|
-- ranks 4+ -> further indented, dimmer
|
||||||
|
--
|
||||||
|
-- We check remaining vertical space and stop when full.
|
||||||
|
|
||||||
|
for i, entry in ipairs(entries) do
|
||||||
|
if row > h then break end
|
||||||
|
|
||||||
|
local rankPrefix
|
||||||
|
local nameColor = RANK_COLORS[i] or DEFAULT_COLOR
|
||||||
|
local valueColor = RANK_COLORS[i] or colors.lightGray
|
||||||
|
|
||||||
|
if i == 1 then
|
||||||
|
rankPrefix = "#1 "
|
||||||
|
elseif i == 2 then
|
||||||
|
rankPrefix = " #2 "
|
||||||
|
elseif i == 3 then
|
||||||
|
rankPrefix = " #3 "
|
||||||
|
else
|
||||||
|
rankPrefix = string.format(" #%-2d", i)
|
||||||
|
end
|
||||||
|
|
||||||
|
local valStr = formatValue(entry.dataType, entry.value)
|
||||||
|
local prefix = rankPrefix
|
||||||
|
local maxName = w - #prefix - #valStr - 1
|
||||||
|
local name = truncate(entry.username, math.max(3, maxName))
|
||||||
|
|
||||||
|
-- Pad so value is right-aligned
|
||||||
|
local line = prefix .. name
|
||||||
|
local spaces = w - #line - #valStr
|
||||||
|
if spaces < 1 then spaces = 1 end
|
||||||
|
line = line .. string.rep(" ", spaces) .. valStr
|
||||||
|
|
||||||
|
-- Draw prefix in rank colour
|
||||||
|
mon.setCursorPos(1, row)
|
||||||
|
mon.setTextColor(nameColor)
|
||||||
|
mon.write(prefix)
|
||||||
|
|
||||||
|
-- Draw name
|
||||||
|
mon.setTextColor(nameColor)
|
||||||
|
mon.write(name)
|
||||||
|
|
||||||
|
-- Draw spaces (neutral)
|
||||||
|
mon.setTextColor(BG_COLOR)
|
||||||
|
mon.write(string.rep(" ", spaces))
|
||||||
|
|
||||||
|
-- Draw value
|
||||||
|
mon.setTextColor(valueColor)
|
||||||
|
mon.write(valStr)
|
||||||
|
|
||||||
|
row = row + 1
|
||||||
|
|
||||||
|
-- Extra blank line after rank 1 for visual separation
|
||||||
|
if i == 1 and row <= h then
|
||||||
|
row = row + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Main loop
|
||||||
|
|
||||||
|
local function main()
|
||||||
|
local data = nil
|
||||||
|
local statIdx = 1
|
||||||
|
local lastFetch = 0 -- start at 0 to force initial fetch
|
||||||
|
|
||||||
|
while true do
|
||||||
|
-- Check if we need to fetch new data (every 5 minutes minimum)
|
||||||
|
local currentTime = os.epoch("utc") / 1000 -- convert to seconds
|
||||||
|
if data == nil or (currentTime - lastFetch) >= FETCH_COOLDOWN then
|
||||||
|
local newData = fetchData()
|
||||||
|
if newData then
|
||||||
|
data = newData
|
||||||
|
lastFetch = currentTime
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local statKey = STAT_KEYS[statIdx]
|
||||||
|
local entries = nil
|
||||||
|
local statLabel = statKey
|
||||||
|
|
||||||
|
if data then
|
||||||
|
entries, statLabel = buildLeaderboard(data, statKey)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Get fresh monitors each tick (in case of reconnect)
|
||||||
|
local monitors = getMonitors()
|
||||||
|
|
||||||
|
if entries then
|
||||||
|
for _, mon in ipairs(monitors) do
|
||||||
|
pcall(renderLeaderboard, mon, entries, statLabel)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- (if no entries for this stat we simply skip drawing & move on)
|
||||||
|
|
||||||
|
-- Advance to next stat
|
||||||
|
statIdx = (statIdx % #STAT_KEYS) + 1
|
||||||
|
|
||||||
|
-- Wait before next stat
|
||||||
|
os.sleep(CYCLE_DELAY)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user