ctf_stats: Add support for retrieving stats by rank (#454)
This commit is contained in:
parent
1f10fd9e22
commit
6b33c8e9cc
4 changed files with 197 additions and 139 deletions
|
@ -2,47 +2,53 @@
|
||||||
-- Helpers --
|
-- Helpers --
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
local function return_as_chat_result(to, name)
|
local function return_as_chat_result(to, target)
|
||||||
local players = {}
|
local players = ctf_stats.get_ordered_players()
|
||||||
for pname, pstat in pairs(ctf_stats.players) do
|
|
||||||
pstat.name = pname
|
local name, place, stat
|
||||||
pstat.color = nil
|
if type(target) == "number" then
|
||||||
table.insert(players, pstat)
|
place = target
|
||||||
|
stat = players[target]
|
||||||
|
name = stat.name
|
||||||
|
elseif type(target) == "string" then
|
||||||
|
-- If target is a string, search through the player stats for a match
|
||||||
|
name = target
|
||||||
|
for i = 1, #players do
|
||||||
|
local pstat = players[i]
|
||||||
|
if pstat.name == name then
|
||||||
|
stat = pstat
|
||||||
|
place = i
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If stat does not exist yet, set place to size of players + 1
|
||||||
|
if place < 1 then
|
||||||
|
place = #players + 1
|
||||||
|
end
|
||||||
|
else
|
||||||
|
error("Invalid type passed to return_as_chat_result!", 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
table.sort(players, function(one, two)
|
-- Build return string
|
||||||
return one.score > two.score
|
local result = (to == name and "You are in " or name .. " is in ") ..
|
||||||
end)
|
place .. " place.\n"
|
||||||
|
|
||||||
local place = -1
|
if stat then
|
||||||
local me = nil
|
local kd = stat.kills
|
||||||
for i = 1, #players do
|
if stat.deaths > 1 then
|
||||||
local pstat = players[i]
|
kd = kd / stat.deaths
|
||||||
if pstat.name == name then
|
|
||||||
me = pstat
|
|
||||||
place = i
|
|
||||||
break
|
|
||||||
end
|
end
|
||||||
end
|
result = result ..
|
||||||
if place < 1 then
|
"Kills: " .. stat.kills ..
|
||||||
place = #players + 1
|
" | Deaths: " .. stat.deaths ..
|
||||||
end
|
|
||||||
local you_are_in = (to == name) and "You are in " or name .. " is in "
|
|
||||||
local result = you_are_in .. place .. " place.\n"
|
|
||||||
if me then
|
|
||||||
local kd = me.kills
|
|
||||||
if me.deaths > 1 then
|
|
||||||
kd = kd / me.deaths
|
|
||||||
end
|
|
||||||
result = result .. "Kills: " .. me.kills ..
|
|
||||||
" | Deaths: " .. me.deaths ..
|
|
||||||
" | K/D: " .. math.floor(kd * 10) / 10 ..
|
" | K/D: " .. math.floor(kd * 10) / 10 ..
|
||||||
"\nBounty kills: " .. me.bounty_kills ..
|
"\nBounty kills: " .. stat.bounty_kills ..
|
||||||
" | Captures: " .. me.captures ..
|
" | Captures: " .. stat.captures ..
|
||||||
" | Attempts: " .. me.attempts ..
|
" | Attempts: " .. stat.attempts ..
|
||||||
"\nScore: " .. math.floor(me.score)
|
"\nScore: " .. math.floor(stat.score)
|
||||||
end
|
end
|
||||||
return true, result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
-------------------
|
-------------------
|
||||||
|
@ -62,56 +68,57 @@ minetest.register_chatcommand("summary", {
|
||||||
})
|
})
|
||||||
|
|
||||||
minetest.register_chatcommand("r", {
|
minetest.register_chatcommand("r", {
|
||||||
params = "[<name>]",
|
params = "[<name> | <rank>]",
|
||||||
description = "Display rankings of yourself or another player as a chat result.",
|
description = "Display rankings of yourself, or another player or rank, as a chat result.",
|
||||||
func = function(name, param)
|
func = function(name, param)
|
||||||
local target
|
local target, error = ctf_stats.get_target(name, param)
|
||||||
if param ~= "" then
|
if not target then
|
||||||
param = param:trim()
|
return false, error
|
||||||
if ctf_stats.players[param] then
|
|
||||||
target = param
|
|
||||||
minetest.log("action", name .. " ran /r " .. param)
|
|
||||||
else
|
|
||||||
return false, "Can't find player '" .. param .. "'"
|
|
||||||
end
|
|
||||||
else
|
|
||||||
target = name
|
|
||||||
minetest.log("action", name .. " ran /r")
|
|
||||||
end
|
end
|
||||||
return return_as_chat_result(name, target)
|
|
||||||
|
minetest.log("action", name .. " runs /r " .. param)
|
||||||
|
return true, return_as_chat_result(name, target)
|
||||||
|
end
|
||||||
|
})
|
||||||
|
|
||||||
|
minetest.register_chatcommand("rn", {
|
||||||
|
params = "<rank>",
|
||||||
|
description = "Display rankings of player at the specified rank.",
|
||||||
|
func = function(name, param)
|
||||||
|
if not param or param == "" then
|
||||||
|
return false, "Empty arguments not allowed! Specify a rank."
|
||||||
|
end
|
||||||
|
|
||||||
|
param = tonumber(param)
|
||||||
|
if not param then
|
||||||
|
return false, "Argument isn't a valid number!"
|
||||||
|
elseif param <= 0 or param > #ctf_stats.get_ordered_players() or
|
||||||
|
param ~= math.floor(param) then
|
||||||
|
return false, "Invalid number or number out of bounds!"
|
||||||
|
-- TODO: This is the worst way to do it. FIX IT.
|
||||||
|
end
|
||||||
|
|
||||||
|
minetest.log("action", name .. " runs /rn " .. param)
|
||||||
|
return true, return_as_chat_result(name, param)
|
||||||
end
|
end
|
||||||
})
|
})
|
||||||
|
|
||||||
minetest.register_chatcommand("rankings", {
|
minetest.register_chatcommand("rankings", {
|
||||||
params = "[<name>]",
|
params = "[<name> | <rank>]",
|
||||||
description = "Display rankings of yourself or another player.",
|
description = "Display rankings of yourself, or another player or rank.",
|
||||||
func = function(name, param)
|
func = function(name, param)
|
||||||
local target
|
local target, error = ctf_stats.get_target(name, param)
|
||||||
if param ~= "" then
|
if not target then
|
||||||
param = param:trim()
|
return false, error
|
||||||
if ctf_stats.players[param] then
|
|
||||||
target = param
|
|
||||||
minetest.log("action", name .. " ran /rankings " .. param)
|
|
||||||
else
|
|
||||||
return false, "Can't find player '" .. param .. "'"
|
|
||||||
end
|
|
||||||
else
|
|
||||||
target = name
|
|
||||||
minetest.log("action", name .. " ran /rankings")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
minetest.log("action", name .. " runs /rankings " .. param)
|
||||||
if not minetest.get_player_by_name(name) then
|
if not minetest.get_player_by_name(name) then
|
||||||
return return_as_chat_result(name, target)
|
return true, return_as_chat_result(name, target)
|
||||||
else
|
else
|
||||||
local players = {}
|
minetest.show_formspec(name, "ctf_stats:rankings", ctf_stats.get_formspec(
|
||||||
for pname, pstat in pairs(ctf_stats.players) do
|
"Player Rankings", ctf_stats.get_ordered_players(), 0, target))
|
||||||
pstat.name = pname
|
return true
|
||||||
pstat.color = nil
|
|
||||||
table.insert(players, pstat)
|
|
||||||
end
|
|
||||||
|
|
||||||
local fs = ctf_stats.get_formspec("Player Rankings", players, 0, target)
|
|
||||||
minetest.show_formspec(name, "ctf_stats:rankings", fs)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
-- Number of entries to display in the player rankings table
|
||||||
|
ctf_stats.rankings_display_count = 50
|
||||||
|
|
||||||
-- Formspec element that governs table columns and their attributes
|
-- Formspec element that governs table columns and their attributes
|
||||||
local tablecolumns = {
|
local tablecolumns = {
|
||||||
"tablecolumns[color;",
|
"tablecolumns[color;",
|
||||||
|
@ -20,8 +23,8 @@ local function render_team_stats(red, blue, stat, round)
|
||||||
blue_stat = math.floor(blue_stat * 10) / 10
|
blue_stat = math.floor(blue_stat * 10) / 10
|
||||||
end
|
end
|
||||||
return red_stat + blue_stat .. " (" ..
|
return red_stat + blue_stat .. " (" ..
|
||||||
minetest.colorize(red.color, tostring(red_stat)) .. " - " ..
|
minetest.colorize(red.color, tostring(red_stat)) .. " - " ..
|
||||||
minetest.colorize(blue.color, tostring(blue_stat)) .. ")"
|
minetest.colorize(blue.color, tostring(blue_stat)) .. ")"
|
||||||
end
|
end
|
||||||
|
|
||||||
function ctf_stats.get_formspec_match_summary(stats, winner_team, winner_player, time)
|
function ctf_stats.get_formspec_match_summary(stats, winner_team, winner_player, time)
|
||||||
|
@ -90,7 +93,7 @@ function ctf_stats.get_formspec_match_summary(stats, winner_team, winner_player,
|
||||||
return ret
|
return ret
|
||||||
end
|
end
|
||||||
|
|
||||||
function ctf_stats.get_formspec(title, players, header, hlt_name)
|
function ctf_stats.get_formspec(title, players, header, target)
|
||||||
table.sort(players, function(one, two)
|
table.sort(players, function(one, two)
|
||||||
return one.score > two.score
|
return one.score > two.score
|
||||||
end)
|
end)
|
||||||
|
@ -105,14 +108,25 @@ function ctf_stats.get_formspec(title, players, header, hlt_name)
|
||||||
ret = ret .. "table[0.5,0;13.25,6.1;scores;"
|
ret = ret .. "table[0.5,0;13.25,6.1;scores;"
|
||||||
ret = ret .. "#ffffff,,Player,Kills,Deaths,K/D,Bounty Kills,Captures,Attempts,Score"
|
ret = ret .. "#ffffff,,Player,Kills,Deaths,K/D,Bounty Kills,Captures,Attempts,Score"
|
||||||
|
|
||||||
local player_in_top_50 = false
|
local hstat, hplace
|
||||||
|
if type(target) == "number" then
|
||||||
|
hstat = players[target]
|
||||||
|
hplace = target
|
||||||
|
elseif type(target) == "string" then
|
||||||
|
for i, stat in pairs(players) do
|
||||||
|
if stat.name == target then
|
||||||
|
hplace = i
|
||||||
|
hstat = stat
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
for i = 1, math.min(#players, 50) do
|
for i = 1, math.min(#players, ctf_stats.rankings_display_count) do
|
||||||
local pstat = players[i]
|
local pstat = players[i]
|
||||||
local color
|
local color
|
||||||
if hlt_name and pstat.name == hlt_name then
|
if hplace == i then
|
||||||
color = "#ffff00"
|
color = "#ffff00"
|
||||||
player_in_top_50 = true
|
|
||||||
else
|
else
|
||||||
color = pstat.color or "#ffffff"
|
color = pstat.color or "#ffffff"
|
||||||
end
|
end
|
||||||
|
@ -121,58 +135,46 @@ function ctf_stats.get_formspec(title, players, header, hlt_name)
|
||||||
kd = kd / pstat.deaths
|
kd = kd / pstat.deaths
|
||||||
end
|
end
|
||||||
ret = ret ..
|
ret = ret ..
|
||||||
"," .. string.gsub(color, "0x", "#") ..
|
"," .. string.gsub(color, "0x", "#") ..
|
||||||
"," .. i ..
|
"," .. i ..
|
||||||
"," .. pstat.name ..
|
"," .. pstat.name ..
|
||||||
"," .. pstat.kills ..
|
"," .. pstat.kills ..
|
||||||
"," .. pstat.deaths ..
|
"," .. pstat.deaths ..
|
||||||
"," .. math.floor(kd * 10) / 10 ..
|
"," .. math.floor(kd * 10) / 10 ..
|
||||||
"," .. pstat.bounty_kills ..
|
"," .. pstat.bounty_kills ..
|
||||||
"," .. pstat.captures ..
|
"," .. pstat.captures ..
|
||||||
"," .. pstat.attempts ..
|
"," .. pstat.attempts ..
|
||||||
"," .. math.floor(pstat.score * 10) / 10
|
"," .. math.floor(pstat.score * 10) / 10
|
||||||
end
|
end
|
||||||
ret = ret .. ";-1]"
|
ret = ret .. ";-1]"
|
||||||
|
|
||||||
-- If hlt_name not in top 50, add a separate table
|
-- If target not in top 50, add a separate table
|
||||||
-- This would result in the player's score displayed at the bottom
|
-- This would result in the player's score displayed at the bottom
|
||||||
-- of the list but yet be visible without having to scroll
|
-- of the list but yet be visible without having to scroll
|
||||||
if hlt_name and not player_in_top_50 then
|
if hplace and hplace > ctf_stats.rankings_display_count then
|
||||||
local hlt_player, hlt_rank, hlt_kd
|
local h_kd = hstat.kills
|
||||||
|
if hstat.deaths > 1 then
|
||||||
for i = 1, #players do
|
h_kd = h_kd / hstat.deaths
|
||||||
if players[i].name == hlt_name then
|
|
||||||
hlt_player = players[i]
|
|
||||||
hlt_rank = i
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if hlt_player then
|
ret = ret .. tablecolumns
|
||||||
hlt_kd = hlt_player.kills
|
ret = ret .. "tableoptions[highlight=#00000000]"
|
||||||
if hlt_player.deaths > 1 then
|
ret = ret .. "table[0.5,6.1;13.25,0.4;hlt_score;"
|
||||||
hlt_kd = hlt_kd / hlt_player.deaths
|
ret = ret .. "#ffff00" ..
|
||||||
end
|
"," .. hplace ..
|
||||||
|
"," .. hstat.name ..
|
||||||
ret = ret .. tablecolumns
|
"," .. hstat.kills ..
|
||||||
ret = ret .. "tableoptions[highlight=#00000000]"
|
"," .. hstat.deaths ..
|
||||||
ret = ret .. "table[0.5,6.1;13.25,0.4;hlt_score;"
|
"," .. math.floor(h_kd * 10) / 10 ..
|
||||||
ret = ret .. "#ffff00" ..
|
"," .. hstat.bounty_kills ..
|
||||||
"," .. hlt_rank ..
|
"," .. hstat.captures ..
|
||||||
"," .. hlt_player.name ..
|
"," .. hstat.attempts ..
|
||||||
"," .. hlt_player.kills ..
|
"," .. math.floor(hstat.score * 10) / 10 .. ";-1]"
|
||||||
"," .. hlt_player.deaths ..
|
--[[ else
|
||||||
"," .. math.floor(hlt_kd * 10) / 10 ..
|
ret = ret .. "box[0.5,6.1;13.25,0.4;#101010]"
|
||||||
"," .. hlt_player.bounty_kills ..
|
Adds a box where the extra table should be, in order to make it
|
||||||
"," .. hlt_player.captures ..
|
appear as an extension of the main table, but the color can't be
|
||||||
"," .. hlt_player.attempts ..
|
matched, and looks slightly brighter or slightly darker than the table]]
|
||||||
"," .. math.floor(hlt_player.score * 10) / 10 .. ";-1]"
|
|
||||||
end
|
|
||||||
-- else
|
|
||||||
-- ret = ret .. "box[0.5,6.1;13.25,0.4;#101010]"
|
|
||||||
-- Adds a box where the extra table should be, in order to make it
|
|
||||||
-- appear as an extension of the main table, but the color can't be
|
|
||||||
-- matched, and looks slightly brighter or slightly darker than the table
|
|
||||||
end
|
end
|
||||||
|
|
||||||
ret = ret .. "button_exit[10,6.5;3,1;close;Close]"
|
ret = ret .. "button_exit[10,6.5;3,1;close;Close]"
|
||||||
|
@ -180,11 +182,8 @@ function ctf_stats.get_formspec(title, players, header, hlt_name)
|
||||||
return ret
|
return ret
|
||||||
end
|
end
|
||||||
|
|
||||||
function ctf_stats.get_html(title, players)
|
function ctf_stats.get_html(title)
|
||||||
table.sort(players, function(one, two)
|
local players = ctf_stats.get_ordered_players()
|
||||||
return one.score > two.score
|
|
||||||
end)
|
|
||||||
|
|
||||||
local ret = "<h1>" .. title .. "</h1>"
|
local ret = "<h1>" .. title .. "</h1>"
|
||||||
ret = ret .. "<table>" ..
|
ret = ret .. "<table>" ..
|
||||||
"<tr><th></th>" ..
|
"<tr><th></th>" ..
|
||||||
|
@ -220,13 +219,6 @@ function ctf_stats.get_html(title, players)
|
||||||
end
|
end
|
||||||
|
|
||||||
function ctf_stats.html_to_file(filepath)
|
function ctf_stats.html_to_file(filepath)
|
||||||
local players = {}
|
|
||||||
for name, pstat in pairs(ctf_stats.players) do
|
|
||||||
pstat.name = name
|
|
||||||
pstat.color = nil
|
|
||||||
table.insert(players, pstat)
|
|
||||||
end
|
|
||||||
local html = ctf_stats.get_html("Player Rankings", players)
|
|
||||||
local f = io.open(filepath, "w")
|
local f = io.open(filepath, "w")
|
||||||
f:write("<!doctype html>\n")
|
f:write("<!doctype html>\n")
|
||||||
f:write("<html><head>\n")
|
f:write("<html><head>\n")
|
||||||
|
@ -234,7 +226,7 @@ function ctf_stats.html_to_file(filepath)
|
||||||
f:write("<title>Player Rankings</title>\n")
|
f:write("<title>Player Rankings</title>\n")
|
||||||
f:write("<link rel=\"stylesheet\" href=\"score_style.css\">\n")
|
f:write("<link rel=\"stylesheet\" href=\"score_style.css\">\n")
|
||||||
f:write("</head><body>\n")
|
f:write("</head><body>\n")
|
||||||
f:write(html)
|
f:write(ctf_stats.get_html("Player Rankings"))
|
||||||
f:write("</body></html>\n")
|
f:write("</body></html>\n")
|
||||||
f:close()
|
f:close()
|
||||||
end
|
end
|
||||||
|
|
|
@ -137,6 +137,64 @@ function ctf_stats.player(name)
|
||||||
return player_stats, match_player_stats
|
return player_stats, match_player_stats
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function ctf_stats.get_ordered_players()
|
||||||
|
local players = {}
|
||||||
|
|
||||||
|
-- Copy player stats into new empty table
|
||||||
|
for pname, pstat in pairs(ctf_stats.players) do
|
||||||
|
pstat.name = pname
|
||||||
|
pstat.color = nil
|
||||||
|
table.insert(players, pstat)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Sort table in the order of descending scores
|
||||||
|
table.sort(players, function(one, two)
|
||||||
|
return one.score > two.score
|
||||||
|
end)
|
||||||
|
|
||||||
|
return players
|
||||||
|
end
|
||||||
|
|
||||||
|
function ctf_stats.get_target(name, param)
|
||||||
|
param = param:trim()
|
||||||
|
|
||||||
|
-- If param is not empty, check if it's a number or a string
|
||||||
|
if param ~= "" then
|
||||||
|
-- Order of the following checks are as given below:
|
||||||
|
--
|
||||||
|
-- * `param` is returned as a string if player's stats exists
|
||||||
|
-- * If no matching stats exist, `param` is checked if it's a number
|
||||||
|
-- * If `param` isn't a number, it is assumed to be invalid, and nil is returned
|
||||||
|
-- * If `param` is a number, `param` is checked if out of bounds
|
||||||
|
-- * If `param` is not out of bounds, `param` is returned as a number, else nil
|
||||||
|
--
|
||||||
|
-- This order of checks is important because, in the case of `param` matching
|
||||||
|
-- both a number and a player name, it would be considered as a player name.
|
||||||
|
|
||||||
|
-- Check if param matches a player name
|
||||||
|
if ctf_stats.players[param] then
|
||||||
|
return param
|
||||||
|
else
|
||||||
|
-- Check if param is a number
|
||||||
|
local rank = tonumber(param)
|
||||||
|
if rank then
|
||||||
|
-- Check if param is within range
|
||||||
|
-- TODO: Fix this hack by maintaining two tables - an ordered list, and a hashmap
|
||||||
|
if rank <= 0 or rank > #ctf_stats.get_ordered_players() or
|
||||||
|
rank ~= math.floor(rank) then
|
||||||
|
return nil, "Invalid number or number out of bounds!"
|
||||||
|
else
|
||||||
|
return rank
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return nil, "Invalid player name specified!"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
ctf.register_on_join_team(function(name, tname)
|
ctf.register_on_join_team(function(name, tname)
|
||||||
ctf_stats.current[tname][name] = ctf_stats.current[tname][name] or {
|
ctf_stats.current[tname][name] = ctf_stats.current[tname][name] or {
|
||||||
kills = 0,
|
kills = 0,
|
||||||
|
|
|
@ -69,6 +69,7 @@ function random_messages.read_messages()
|
||||||
"Help your team claim victory by storing extra weapons in the team chest, and never taking more than you need.",
|
"Help your team claim victory by storing extra weapons in the team chest, and never taking more than you need.",
|
||||||
"Excessive spawn-killing is a direct violation of the rules - appropriate punishments will be given.",
|
"Excessive spawn-killing is a direct violation of the rules - appropriate punishments will be given.",
|
||||||
"Use /r to check your score and rank, and /rankings to see the league tables.",
|
"Use /r to check your score and rank, and /rankings to see the league tables.",
|
||||||
|
"Use /r <number> or /rn <number> to check the rankings of the player in the given rank.",
|
||||||
"Use bandages on team-mates to heal them by 3-4 HP if their health is below 15 HP.",
|
"Use bandages on team-mates to heal them by 3-4 HP if their health is below 15 HP.",
|
||||||
"Use /m to add a team marker at pointed location, that's visible only to team-mates.",
|
"Use /m to add a team marker at pointed location, that's visible only to team-mates.",
|
||||||
"Use /summary to check scores of the current match and the previous match.",
|
"Use /summary to check scores of the current match and the previous match.",
|
||||||
|
|
Loading…
Reference in a new issue