Reorganise game into modpacks

This commit is contained in:
rubenwardy 2019-01-11 15:45:27 +00:00
parent 86a5266bb5
commit b38a89c2fe
762 changed files with 9 additions and 8 deletions

View file

@ -0,0 +1,2 @@
ctf
ctf_match

393
mods/ctf/ctf_stats/gui.lua Normal file
View file

@ -0,0 +1,393 @@
-- Formspec element that governs table columns and their attributes
local tablecolumns = {
"tablecolumns[color;",
"text;",
"text,width=20;",
"text,width=4;",
"text,width=4;",
"text,width=4;",
"text,width=6;",
"text,width=6;",
"text,width=6;",
"text,width=6]"
}
tablecolumns = table.concat(tablecolumns, "")
local function render_team_stats(red, blue, stat, round)
local red_stat, blue_stat = red[stat], blue[stat]
if round then
red_stat = math.floor(red_stat * 10) / 10
blue_stat = math.floor(blue_stat * 10) / 10
end
return red_stat + blue_stat .. " (" ..
minetest.colorize(red.color, tostring(red_stat)) .. " - " ..
minetest.colorize(blue.color, tostring(blue_stat)) .. ")"
end
function ctf_stats.get_formspec_match_summary(stats, winner_team, winner_player, time)
local players = {}
local red = {
color = ctf.flag_colors.red:gsub("0x", "#"),
kills = 0,
attempts = 0,
score = 0,
}
local blue = {
color = ctf.flag_colors.blue:gsub("0x", "#"),
kills = 0,
attempts = 0,
score = 0,
}
for name, pstat in pairs(stats.red) do
pstat.name = name
pstat.color = ctf.flag_colors.red
table.insert(players, pstat)
red.kills = red.kills + pstat.kills
red.attempts = red.attempts + pstat.attempts
red.score = red.score + pstat.score
end
for name, pstat in pairs(stats.blue) do
pstat.name = name
pstat.color = ctf.flag_colors.blue
table.insert(players, pstat)
blue.kills = blue.kills + pstat.kills
blue.attempts = blue.attempts + pstat.attempts
blue.score = blue.score + pstat.score
end
local match_length = string.format("%02d:%02d:%02d",
math.floor(time / 3600), -- hours
math.floor((time % 3600) / 60), -- minutes
math.floor(time % 60)) -- seconds
local ret = ctf_stats.get_formspec("Match Summary", players, 1)
if stats[winner_team] then
local winner_color = ctf.flag_colors[winner_team]:gsub("0x", "#")
ret = ret .. "item_image[0,0;1,1;ctf_flag:flag_top_"..winner_team.."]"
ret = ret .. "label[1,0;" .. minetest.colorize(winner_color,
"TEAM " .. winner_team:upper() .. " WON!") .. "]"
ret = ret .. "label[1,0.5;Flag captured by " ..
minetest.colorize(winner_color, winner_player) .. "]"
else
ret = ret .. "label[1,0;NO WINNER]"
end
ret = ret .. "label[6.5,0;Kills]"
ret = ret .. "label[8,0;" .. render_team_stats(red, blue, "kills") .. "]"
ret = ret .. "label[6.5,0.5;Attempts]"
ret = ret .. "label[8,0.5;" .. render_team_stats(red, blue, "attempts") .. "]"
ret = ret .. "label[10.5,0;Duration]"
ret = ret .. "label[12,0;" .. match_length .. "]"
ret = ret .. "label[10.5,0.5;Total score]"
ret = ret .. "label[12,0.5;" .. render_team_stats(red, blue, "score", true) .. "]"
ret = ret .. "label[2,7.75;Tip: type /rankings for league tables]"
return ret
end
function ctf_stats.get_formspec(title, players, header, hlt_name)
table.sort(players, function(one, two)
return one.score > two.score
end)
local ret = "size[14," .. 7 + header .. "]"
ret = ret .. default.gui_bg .. default.gui_bg_img
ret = ret .. "container[0," .. header .. "]"
ret = ret .. "vertlabel[0,1;" .. title .. "]"
ret = ret .. tablecolumns
ret = ret .. "tableoptions[highlight=#00000000]"
ret = ret .. "table[0.5,0;13.25,6.1;scores;"
ret = ret .. "#ffffff,,Player,Kills,Deaths,K/D,Bounty Kills,Captures,Attempts,Score"
local player_in_top_50 = false
for i = 1, math.min(#players, 50) do
local pstat = players[i]
local color
if hlt_name and pstat.name == hlt_name then
color = "#ffff00"
player_in_top_50 = true
else
color = pstat.color or "#ffffff"
end
local kd = pstat.kills
if pstat.deaths > 1 then
kd = kd / pstat.deaths
end
ret = ret ..
"," .. string.gsub(color, "0x", "#") ..
"," .. i ..
"," .. pstat.name ..
"," .. pstat.kills ..
"," .. pstat.deaths ..
"," .. math.floor(kd * 10) / 10 ..
"," .. pstat.bounty_kills ..
"," .. pstat.captures ..
"," .. pstat.attempts ..
"," .. math.floor(pstat.score * 10) / 10
end
ret = ret .. ";-1]"
-- If hlt_name not in top 50, add a separate table
-- This would result in the player's score displayed at the bottom
-- of the list but yet be visible without having to scroll
if hlt_name and not player_in_top_50 then
local hlt_player, hlt_rank, hlt_kd
for i = 1, #players do
if players[i].name == hlt_name then
hlt_player = players[i]
hlt_rank = i
break
end
end
if hlt_player then
hlt_kd = hlt_player.kills
if hlt_player.deaths > 1 then
hlt_kd = hlt_kd / hlt_player.deaths
end
ret = ret .. tablecolumns
ret = ret .. "tableoptions[highlight=#00000000]"
ret = ret .. "table[0.5,6.1;13.25,0.4;hlt_score;"
ret = ret .. "#ffff00" ..
"," .. hlt_rank ..
"," .. hlt_player.name ..
"," .. hlt_player.kills ..
"," .. hlt_player.deaths ..
"," .. math.floor(hlt_kd * 10) / 10 ..
"," .. hlt_player.bounty_kills ..
"," .. hlt_player.captures ..
"," .. hlt_player.attempts ..
"," .. 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
ret = ret .. "button_exit[10,6.5;3,1;close;Close]"
ret = ret .. "container_end[]"
return ret
end
function ctf_stats.get_html(title, players)
table.sort(players, function(one, two)
return one.score > two.score
end)
local ret = "<h1>" .. title .. "</h1>"
ret = ret .. "<table>" ..
"<tr><th></th>" ..
"<th>Player</th>" ..
"<th>Kills</th>" ..
"<th>Deaths</th>" ..
"<th>K/D ratio</th>" ..
"<th>Bounty kills</th>" ..
"<th>Captures</th>" ..
"<th>Attempts</th>" ..
"<th>Score</th></tr>"
for i = 1, math.min(#players, 50) do
local pstat = players[i]
local kd = pstat.kills
if pstat.deaths > 1 then
kd = kd / pstat.deaths
end
ret = ret ..
"<tr><td>" .. i ..
"</td><td>" .. pstat.name ..
"</td><td>" .. pstat.kills ..
"</td><td>" .. pstat.deaths ..
"</td><td>" .. math.floor(kd * 10) / 10 ..
"</td><td>" .. pstat.bounty_kills ..
"</td><td>" .. pstat.captures ..
"</td><td>" .. pstat.attempts ..
"</td><td>" .. math.floor(pstat.score*10)/10 .. "</td></tr>"
end
ret = ret .. "</table>\n"
return ret
end
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")
f:write("<!doctype html>\n")
f:write("<html><head>\n")
f:write("<meta charset=\"utf-8\">\n")
f:write("<title>Player Rankings</title>\n")
f:write("<link rel=\"stylesheet\" href=\"score_style.css\">\n")
f:write("</head><body>\n")
f:write(html)
f:write("</body></html>\n")
f:close()
end
local function return_as_chat_result(to, name)
local players = {}
for pname, pstat in pairs(ctf_stats.players) do
pstat.name = pname
pstat.color = nil
table.insert(players, pstat)
end
table.sort(players, function(one, two)
return one.score > two.score
end)
local place = -1
local me = nil
for i = 1, #players do
local pstat = players[i]
if pstat.name == name then
me = pstat
place = i
break
end
end
if place < 1 then
place = #players + 1
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 ..
"\nBounty kills: " .. me.bounty_kills ..
" | Captures: " .. me.captures ..
" | Attempts: " .. me.attempts ..
"\nScore: " .. math.floor(me.score)
end
return true, result
end
minetest.register_chatcommand("r", {
params = "[<name>]",
description = "Display rankings of yourself or another player as a chat result.",
func = function(name, param)
local target
if param ~= "" then
param = param:trim()
if ctf_stats.players[param] then
target = param
else
return false, "Can't find player '" .. param .. "'"
end
else
target = name
end
return return_as_chat_result(name, target)
end
})
minetest.register_chatcommand("rankings", {
params = "[<name>]",
description = "Display rankings of yourself or another player.",
func = function(name, param)
local target
if param ~= "" then
param = param:trim()
if ctf_stats.players[param] then
target = param
else
return false, "Can't find player '" .. param .. "'"
end
else
target = name
end
if not minetest.get_player_by_name(name) then
return return_as_chat_result(name, target)
else
local players = {}
for pname, pstat in pairs(ctf_stats.players) do
pstat.name = pname
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
})
local reset_y = {}
minetest.register_chatcommand("reset_rankings", {
params = "[<name>]",
description = "Reset the rankings of yourself or another player",
func = function(name, param)
param = param:trim()
if param ~= "" and not minetest.check_player_privs(name, {ctf_admin = true}) then
return false, "Missing privilege: ctf_admin"
end
local reset_name = param == "" and name or param
if not ctf_stats.players[reset_name] then
return false, "Player '" .. reset_name .. "' does not exist."
end
if reset_name == name and not reset_y[name] then
reset_y[name] = true
minetest.after(30, function()
reset_y[name] = nil
end)
return true, "This will reset your stats and rankings completely."
.. " You will lose access to any special privileges such as the"
.. " team chest or userlimit skip. This is irreversable. If you're"
.. " sure, re-type /reset_rankings within 30 seconds to reset."
end
reset_y[name] = nil
ctf_stats.players[reset_name] = nil
ctf_stats.player(reset_name)
ctf.needs_save = true
return true, "Successfully reset the stats and ranking of " .. reset_name
end
})
minetest.register_chatcommand("transfer_rankings", {
params = "<src> <dest>",
description = "Transfer rankings of one player to another.",
privs = {ctf_admin = true},
func = function(name, param)
if not param then
return false, "Invalid syntax. Provide source and destination player names."
end
param = param:trim()
local src, dest = param:trim():match("([%a%d_-]+) ([%a%d_-]+)")
if not src or not dest then
return false, "Invalid usage, see /help transfer_rankings"
end
if not ctf_stats.players[src] then
return false, "Player '" .. src .. "' does not exist."
end
if not ctf_stats.players[dest] then
return false, "Player '" .. dest .. "' does not exist."
end
ctf_stats.players[dest] = ctf_stats.players[src]
ctf_stats.players[src] = nil
ctf.needs_save = true
return true, "Stats of '" .. src .. "' have been transferred to '" .. dest .. "'."
end
})

327
mods/ctf/ctf_stats/init.lua Normal file
View file

@ -0,0 +1,327 @@
ctf_stats = {}
local storage = minetest.get_mod_storage()
local data_to_persist = { "matches", "players" }
function ctf_stats.load_legacy()
local file = io.open(minetest.get_worldpath() .. "/ctf_stats.txt", "r")
if not file then
return false
end
local table = minetest.deserialize(file:read("*all"))
file:close()
if type(table) ~= "table" then
return false
end
ctf.log("ctf_stats", "Migrating stats...")
ctf_stats.matches = table.matches
ctf_stats.players = table.players
for name, player_stats in pairs(ctf_stats.players) do
if not player_stats.score or player_stats.score < 0 then
player_stats.score = 0
end
if player_stats.score > 300 then
player_stats.score = (player_stats.score - 300) / 30 + 300
end
if player_stats.score > 800 then
player_stats.score = 800
end
player_stats.wins = player_stats.wins or {}
if player_stats.blue_wins then
player_stats.wins.blue = player_stats.blue_wins
player_stats.blue_wins = nil
end
if player_stats.red_wins then
player_stats.wins.red = player_stats.red_wins
player_stats.red_wins = nil
end
player_stats.wins.blue = player_stats.wins.blue or 0
player_stats.wins.red = player_stats.wins.red or 0
end
ctf_stats.matches.wins = ctf_stats.matches.wins or {
red = ctf_stats.matches.red_wins or 0,
blue = ctf_stats.matches.blue_wins or 0,
}
ctf.needs_save = true
os.remove(minetest.get_worldpath() .. "/ctf_stats.txt")
return true
end
function ctf_stats.load()
if not ctf_stats.load_legacy() then
for _, key in pairs(data_to_persist) do
ctf_stats[key] = minetest.parse_json(storage:get_string(key))
end
ctf.needs_save = true
end
-- Make sure all tables are present
ctf_stats.players = ctf_stats.players or {}
ctf_stats.matches = ctf_stats.matches or {
wins = {
blue = 0,
red = 0,
},
skipped = 0,
}
ctf_stats.current = ctf_stats.current or {
red = {},
blue = {}
}
ctf_stats.start = os.time()
-- Strip players which have no score
for name, player_stats in pairs(ctf_stats.players) do
if not player_stats.score or player_stats.score <= 0 then
ctf_stats.players[name] = nil
ctf.needs_save = true
else
player_stats.bounty_kills = player_stats.bounty_kills or 0
end
end
end
ctf.register_on_save(function()
for _, key in pairs(data_to_persist) do
storage:set_string(key, minetest.write_json(ctf_stats[key]))
end
return nil
end)
function ctf_stats.player_or_nil(name)
return ctf_stats.players[name], ctf_stats.current.red[name] or ctf_stats.current.blue[name]
end
-- Returns a tuple: `player_stats`, `match_player_stats`
function ctf_stats.player(name)
local player_stats = ctf_stats.players[name]
if not player_stats then
player_stats = {
name = name,
wins = {
red = 0,
blue = 0,
},
kills = 0,
deaths = 0,
captures = 0,
attempts = 0,
score = 0,
bounty_kills = 0,
}
ctf_stats.players[name] = player_stats
end
local match_player_stats =
ctf_stats.current.red[name] or ctf_stats.current.blue[name]
return player_stats, match_player_stats
end
ctf.register_on_join_team(function(name, tname)
ctf_stats.current[tname][name] = ctf_stats.current[tname][name] or {
kills = 0,
kills_since_death = 0,
deaths = 0,
attempts = 0,
captures = 0,
score = 0,
bounty_kills = 0,
}
end)
local winner_team = "-"
local winner_player = "-"
table.insert(ctf_flag.registered_on_capture, 1, function(name, flag)
local main, match = ctf_stats.player(name)
if main and match then
main.captures = main.captures + 1
main.score = main.score + 25
match.captures = match.captures + 1
match.score = match.score + 25
ctf.needs_save = true
end
winner_player = name
end)
local prev_match_summary = storage:get_string("prev_match_summary")
ctf_match.register_on_winner(function(winner)
ctf.needs_save = true
ctf_stats.matches.wins[winner] = ctf_stats.matches.wins[winner] + 1
winner_team = winner
-- Show match summary
local fs = ctf_stats.get_formspec_match_summary(ctf_stats.current,
winner_team, winner_player, os.time() - ctf_stats.start)
for _, player in pairs(minetest.get_connected_players()) do
minetest.show_formspec(player:get_player_name(), "ctf_stats:eom", fs)
end
-- Set prev_match_summary and write to mod_storage
prev_match_summary = fs
storage:set_string("prev_match_summary", fs)
end)
ctf_match.register_on_skip_map(function()
ctf.needs_save = true
ctf_stats.matches.skipped = ctf_stats.matches.skipped + 1
-- Show match summary
local fs = ctf_stats.get_formspec_match_summary(ctf_stats.current,
winner_team, winner_player, os.time()-ctf_stats.start)
for _, player in pairs(minetest.get_connected_players()) do
minetest.show_formspec(player:get_player_name(), "ctf_stats:eom", fs)
end
-- Set prev_match_summary and write to mod_storage
prev_match_summary = fs
storage:set_string("prev_match_summary", fs)
end)
ctf_match.register_on_new_match(function()
ctf_stats.current = {
red = {},
blue = {}
}
winner_team = "-"
winner_player = "-"
ctf_stats.start = os.time()
ctf.needs_save = true
end)
ctf_flag.register_on_pick_up(function(name, flag)
local main, match = ctf_stats.player(name)
if main and match then
main.attempts = main.attempts + 1
main.score = main.score + 5
match.attempts = match.attempts + 1
match.score = match.score + 10
ctf.needs_save = true
end
end)
ctf_flag.register_on_precapture(function(name, flag)
local tplayer = ctf.player(name)
local main, _ = ctf_stats.player(name)
if main then
main.wins[tplayer.team] = main.wins[tplayer.team] + 1
ctf.needs_save = true
end
return true
end)
-- good_weapons now includes all mese and diamond implements, and swords of steel and better
local good_weapons = {
"default:sword_steel",
"default:sword_bronze",
"default:sword_mese",
"default:sword_diamond",
"default:pick_mese",
"default:pick_diamond",
"default:axe_mese",
"default:axe_diamond",
"default:shovel_mese",
"default:shovel_diamond",
"shooter:grenade",
"shooter:shotgun",
"shooter:rifle",
"shooter:machine_gun",
}
local function invHasGoodWeapons(inv)
for _, weapon in pairs(good_weapons) do
if inv:contains_item("main", weapon) then
return true
end
end
return false
end
local function calculateKillReward(victim, killer)
local vmain, victim_match = ctf_stats.player(victim)
-- +5 for every kill they've made since last death in this match.
local reward = victim_match.kills_since_death * 5
ctf.log("ctf_stats", "Player " .. victim .. " has made " .. reward ..
" score worth of kills since last death")
-- 30 * K/D ratio, with variable based on player's score
local kdreward = 30 * vmain.kills / (vmain.deaths + 1)
local max = vmain.score / 6
if kdreward > max then
kdreward = max
end
if kdreward > 80 then
kdreward = 80
end
reward = reward + kdreward
-- Limited to 0 <= X <= 200
if reward > 200 then
reward = 200
elseif reward < 14 then
reward = 14
end
-- Half if no good weapons
local inv = minetest.get_inventory({ type="player", name = victim })
if not invHasGoodWeapons(inv) then
ctf.log("ctf_stats", "Player " .. victim .. " has no good weapons")
reward = reward * 0.5
else
ctf.log("ctf_stats", "Player " .. victim .. " has good weapons")
end
return reward
end
ctf.register_on_killedplayer(function(victim, killer)
-- Suicide is not encouraged here at CTF
if victim == killer then
return
end
local main, match = ctf_stats.player(killer)
if main and match then
local reward = calculateKillReward(victim, killer)
main.kills = main.kills + 1
main.score = main.score + reward
match.kills = match.kills + 1
match.score = match.score + reward
match.kills_since_death = match.kills_since_death + 1
ctf.needs_save = true
end
end)
minetest.register_on_dieplayer(function(player)
local main, match = ctf_stats.player(player:get_player_name())
if main and match then
main.deaths = main.deaths + 1
match.deaths = match.deaths + 1
match.kills_since_death = 0
ctf.needs_save = true
end
end)
minetest.register_chatcommand("summary", {
func = function (name, param)
if not prev_match_summary then
return false, "Couldn't find the requested data."
end
minetest.show_formspec(name, "ctf_stats:prev_match_summary", prev_match_summary)
end
})
ctf_stats.load()
dofile(minetest.get_modpath("ctf_stats").."/gui.lua")