fbf0126599
* Fix bugs with more than two teams * Fix team allocation * Fix bugz * Fix crash * Fix crash with crash fix
454 lines
12 KiB
Lua
454 lines
12 KiB
Lua
ctf_stats = {}
|
|
|
|
local _needs_save = false
|
|
local storage = minetest.get_mod_storage()
|
|
local data_to_persist = { "matches", "players" }
|
|
|
|
ctf_stats.prev_match_summary = storage:get_string("prev_match_summary")
|
|
|
|
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,
|
|
}
|
|
|
|
_needs_save = true
|
|
|
|
os.remove(minetest.get_worldpath() .. "/ctf_stats.txt")
|
|
return true
|
|
end
|
|
|
|
-- Load persistant data from mod storage (or legacy file)
|
|
-- and initialize empty tables where required
|
|
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
|
|
_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 = {}
|
|
}
|
|
|
|
-- 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
|
|
_needs_save = true
|
|
else
|
|
player_stats.bounty_kills = player_stats.bounty_kills or 0
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Save persistant data to mod storage
|
|
function ctf_stats.save()
|
|
for _, key in pairs(data_to_persist) do
|
|
storage:set_string(key, minetest.write_json(ctf_stats[key]))
|
|
end
|
|
end
|
|
|
|
-- Separate recursion to check if save required and then call ctf_stats.save
|
|
-- This allows ctf_stats.save to be called directly when an immediate save is required
|
|
local function check_if_save_needed()
|
|
if _needs_save then
|
|
ctf_stats.save()
|
|
_needs_save = false
|
|
end
|
|
minetest.after(13, check_if_save_needed)
|
|
end
|
|
minetest.after(13, check_if_save_needed)
|
|
|
|
-- API function to allow other mods to request a save
|
|
-- TODO: This should be done automatically once a proper API is in place
|
|
function ctf_stats.request_save()
|
|
_needs_save = true
|
|
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
|
|
|
|
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
|
|
|
|
function ctf_stats.is_pro(name)
|
|
local stats = ctf_stats.player(name)
|
|
local kd = stats.kills / (stats.deaths == 0 and 1 or stats.deaths)
|
|
return stats.score >= 10000 and kd >= 1.5
|
|
end
|
|
|
|
ctf.register_on_join_team(function(name, tname, oldteam)
|
|
if not ctf_stats.current[tname] then
|
|
ctf_stats.current[tname] = {}
|
|
end
|
|
|
|
if oldteam and ctf_stats.current[oldteam] then
|
|
ctf_stats.current[oldteam][name] = nil
|
|
end
|
|
|
|
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)
|
|
|
|
ctf_stats.winner_team = "-"
|
|
ctf_stats.winner_player = "-"
|
|
|
|
table.insert(ctf_flag.registered_on_capture, 1, function(name, flag)
|
|
local score = 0
|
|
for i, pstat in pairs(ctf_stats.current.red) do
|
|
score = score + pstat.score
|
|
end
|
|
for i, pstat in pairs(ctf_stats.current.blue) do
|
|
score = score + pstat.score
|
|
end
|
|
local capturereward = math.floor(score * 10) / 100
|
|
if capturereward < 50 then capturereward = 50 end
|
|
if capturereward > 750 then capturereward = 750 end
|
|
|
|
local main, match = ctf_stats.player(name)
|
|
if main and match then
|
|
main.captures = main.captures + 1
|
|
main.score = main.score + capturereward
|
|
match.captures = match.captures + 1
|
|
match.score = match.score + capturereward
|
|
_needs_save = true
|
|
end
|
|
ctf_stats.winner_player = name
|
|
|
|
hud_score.new(name, {
|
|
name = "ctf_stats:flag_capture",
|
|
color = "0xFF00FF",
|
|
value = capturereward
|
|
})
|
|
end)
|
|
|
|
ctf_match.register_on_winner(function(winner)
|
|
ctf_stats.matches.wins[winner] = ctf_stats.matches.wins[winner] + 1
|
|
ctf_stats.winner_team = winner
|
|
|
|
-- Show match summary
|
|
local fs = ctf_stats.get_formspec_match_summary(ctf_stats.current,
|
|
ctf_stats.winner_team, ctf_stats.winner_player, ctf_match.get_match_duration())
|
|
|
|
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
|
|
ctf_stats.prev_match_summary = fs
|
|
storage:set_string("prev_match_summary", fs)
|
|
|
|
-- Flush data to mod_storage at the end of each match
|
|
ctf_stats.save()
|
|
end)
|
|
|
|
ctf_match.register_on_skip_map(function()
|
|
ctf_stats.matches.skipped = ctf_stats.matches.skipped + 1
|
|
|
|
-- Show match summary
|
|
local fs = ctf_stats.get_formspec_match_summary(ctf_stats.current,
|
|
ctf_stats.winner_team, ctf_stats.winner_player, ctf_match.get_match_duration())
|
|
|
|
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
|
|
ctf_stats.prev_match_summary = fs
|
|
storage:set_string("prev_match_summary", fs)
|
|
|
|
ctf_stats.save()
|
|
end)
|
|
|
|
ctf_match.register_on_new_match(function()
|
|
ctf_stats.current = {
|
|
red = {},
|
|
blue = {}
|
|
}
|
|
ctf_stats.winner_team = "-"
|
|
ctf_stats.winner_player = "-"
|
|
_needs_save = true
|
|
end)
|
|
|
|
-- ctf_map can't be added as a dependency, as that'd result
|
|
-- in cyclic dependencies between ctf_map and ctf_stats
|
|
minetest.after(0, function()
|
|
ctf_map.register_on_map_loaded(function(map)
|
|
ctf_stats.current.map = map.name
|
|
end)
|
|
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 + 20
|
|
match.attempts = match.attempts + 1
|
|
match.score = match.score + 20
|
|
_needs_save = true
|
|
end
|
|
|
|
hud_score.new(name, {
|
|
name = "ctf_stats:flag_pick_up",
|
|
color = "0xAA00AA",
|
|
value = 20
|
|
})
|
|
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
|
|
_needs_save = true
|
|
end
|
|
return true
|
|
end)
|
|
|
|
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",
|
|
"sniper_rifles:rifle_762",
|
|
"sniper_rifles:rifle_magnum",
|
|
}
|
|
|
|
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)
|
|
|
|
if not vmain or not victim_match then return 5 end
|
|
|
|
-- +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 / 5
|
|
if kdreward > max then
|
|
kdreward = max
|
|
end
|
|
if kdreward > 100 then
|
|
kdreward = 100
|
|
end
|
|
reward = reward + kdreward
|
|
|
|
-- Limited to 5 <= X <= 250
|
|
if reward > 250 then
|
|
reward = 250
|
|
elseif reward < 5 then
|
|
reward = 5
|
|
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
|
|
_needs_save = true
|
|
|
|
reward = math.floor(reward * 100) / 100
|
|
|
|
hud_score.new(killer, {
|
|
name = "ctf_stats:kill_score",
|
|
color = "0x00FF00",
|
|
value = reward
|
|
})
|
|
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
|
|
_needs_save = true
|
|
end
|
|
end)
|
|
|
|
ctf_stats.load()
|
|
|
|
dofile(minetest.get_modpath("ctf_stats") .. "/gui.lua")
|
|
dofile(minetest.get_modpath("ctf_stats") .. "/chat.lua")
|