Files
factorio-expcluster/exp_scenario/module/gui/player_stats.lua
T
bbassieandClaude Opus 5.5 ad1c83def8 Address review on the statistics plugin
The plugin is now exp_player_statistics (@expcluster/player-statistics),
with the module, global, permission, route and database directory
renamed to match. The exp-statistics locale section keeps its name.

Every statistic is tracked unconditionally, so the enable flags are gone
from config.lua. Playtime and JoinCount are no longer counted in game:
clusterio already keeps online time and joins for each user, so the
controller fills them in from the user when a player's totals are
requested, and the web page does the same from the user list. They are
not stored or adjusted by the plugin.

The web table has a column picker, a multiple select with check boxes,
Select all and Reset buttons, showing "N statistics selected". The
choice is kept in local storage. The user page shows the statistics of
the player in a grid like the core player stats.

The readme statistics table drops its second title and uses the short
time format.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 23:36:00 +00:00

221 lines
8.6 KiB
Lua

--[[-- Gui - Player Data
Displays the player data for a player
]]
local ExpUtil = require("modules/exp_util")
local Gui = require("modules/exp_gui")
local ElementsExtra = require("modules/exp_scenario/gui/elements")
local Roles = require("modules/exp_roles")
local ExpPlayerStatistics = require("modules/exp_player_statistics")
--- @class ExpGui_PlayerStats.elements
local Elements = {}
local short_time_format = ExpUtil.format_time_factory_locale{ format = "short", coefficient = 3600, hours = true, minutes = true }
local format_number = require("util").format_number
local function format_number_2dp(n)
return format_number(math.floor(n), false) .. string.format("%.2f", n % 1):sub(2)
end
local short_time_zero, format_number_zero = short_time_format(0), format_number_2dp(0)
--- Get a statistic of a player, or the default when it is zero
--- @param player LuaPlayer
--- @param statistic string
--- @param default number
--- @return number
local function get_stat(player, statistic, default)
local value = ExpPlayerStatistics.get(player, statistic)
return value == 0 and default or value
end
--- @type table<string, { default: LocalisedString, calculate: fun(player: LuaPlayer): LocalisedString }>
local computed_stats = {
DamageDeathRatio = {
default = format_number_zero,
calculate = function(player)
return format_number_2dp(get_stat(player, "DamageDealt", 0) / get_stat(player, "Deaths", 1))
end,
},
KillDeathRatio = {
default = format_number_zero,
calculate = function(player)
return format_number_2dp(get_stat(player, "Kills", 0) / get_stat(player, "Deaths", 1))
end,
},
SessionTime = {
default = short_time_zero,
calculate = function(player)
return short_time_format((get_stat(player, "Playtime", 0) - get_stat(player, "AfkTime", 0)) / get_stat(player, "JoinCount", 1))
end,
},
BuildRatio = {
default = format_number_zero,
calculate = function(player)
return format_number_2dp(get_stat(player, "MachinesBuilt", 0) / get_stat(player, "MachinesRemoved", 1))
end,
},
RocketPerHour = {
default = format_number_zero,
calculate = function(player)
return format_number_2dp(get_stat(player, "RocketsLaunched", 0) * 60 / get_stat(player, "Playtime", 1))
end,
},
TreeKillPerMinute = {
default = format_number_zero,
calculate = function(player)
return format_number_2dp(get_stat(player, "TreesDestroyed", 0) / get_stat(player, "Playtime", 1))
end,
},
NetPlayTime = {
default = short_time_zero,
calculate = function(player)
return short_time_format((get_stat(player, "Playtime", 0) - get_stat(player, "AfkTime", 0)))
end,
},
AFKTimeRatio = {
default = format_number_zero,
calculate = function(player)
return format_number_2dp(get_stat(player, "AfkTime", 0) * 100 / get_stat(player, "Playtime", 1))
end,
},
Locale = {
default = "en",
calculate = function(player)
return player.locale
end,
},
}
--- Label used for all data in the data table
--- @class ExpGui_PlayerStats.elements.table_label: ExpElement
--- @overload fun(parent: LuaGuiElement, opts: { caption: LocalisedString, tooltip: LocalisedString, width: number })
Elements.table_label = Gui.define("player_stats/table_label")
:draw{
type = "label",
caption = Gui.from_argument("caption"),
tooltip = Gui.from_argument("tooltip"),
style = "heading_2_label",
}
:style{
width = Gui.from_argument("width"),
} --[[@as any]]
--- Data table that shows all data for a player
--- @class ExpGui_PlayerStats.elements.player_stats_table: ExpElement
--- @field data table<LuaGuiElement, { [string]: LuaGuiElement }>
--- @overload fun(parent: LuaGuiElement): LuaGuiElement
Elements.player_stats_table = Gui.define("player_stats/data_table")
:draw(function(def, parent)
--- @cast def ExpGui_PlayerStats.elements.player_stats_table
local data_table = Gui.elements.scroll_table(parent, 240, 4)
local labels = {}
-- Add all standalone stats
for _, stat_name in ipairs(ExpPlayerStatistics.display_order) do
Elements.table_label(data_table, {
caption = { "exp-statistics." .. stat_name },
tooltip = { "exp-statistics." .. stat_name .. "-tooltip" },
width = 135,
})
labels[stat_name] = Elements.table_label(data_table, {
caption = { "exp-gui_readme.data-format", ExpPlayerStatistics.format(stat_name, 0, true), ExpPlayerStatistics.units[stat_name] or "" },
tooltip = { "exp-statistics." .. stat_name .. "-tooltip" },
width = 105,
})
end
-- Add all computed stats
for stat_name, data in pairs(computed_stats) do
Elements.table_label(data_table, {
caption = { "exp-statistics." .. stat_name },
tooltip = { "exp-statistics." .. stat_name .. "-tooltip" },
width = 135,
})
labels[stat_name] = Elements.table_label(data_table, {
caption = { "exp-gui_readme.data-format", data.default, "" },
tooltip = { "exp-statistics." .. stat_name .. "-tooltip" },
width = 105,
})
end
def.data[data_table] = labels
return data_table
end) --[[@as any]]
--- Refresh a data table with the most recent stats for a player
--- @param data_table LuaGuiElement
--- @param player LuaPlayer
function Elements.player_stats_table.refresh(data_table, player)
local labels = Elements.player_stats_table.data[data_table]
-- Update all standalone stats
for _, stat_name in ipairs(ExpPlayerStatistics.display_order) do
local value = ExpPlayerStatistics.format(stat_name, ExpPlayerStatistics.get(player, stat_name), true)
labels[stat_name].caption = { "exp-gui_readme.data-format", value, ExpPlayerStatistics.units[stat_name] or "" }
end
-- Update all computed stats
for stat_name, data in pairs(computed_stats) do
labels[stat_name].caption = { "exp-gui_readme.data-format", data.calculate(player), "" }
end
end
--- Dropdown which sets the target player
--- @class ExpGui_PlayerStats.elements.player_dropdown: ExpElement
--- @field data table<LuaGuiElement, LuaGuiElement>
--- @overload fun(parent: LuaGuiElement, camera: LuaGuiElement): LuaGuiElement
Elements.player_dropdown = Gui.define("player_stats/player_dropdown")
:track_all_elements()
:draw(function(def, parent)
return ElementsExtra.online_player_dropdown(parent)
end)
:element_data(
Gui.from_argument(1)
)
:on_selection_state_changed(function(def, player, element, event)
--- @cast def ExpGui_PlayerStats.elements.player_dropdown
local data_table = def.data[element]
local target_player = ElementsExtra.online_player_dropdown.get_selected(element)
Elements.player_stats_table.refresh(data_table, target_player)
end) --[[@as any]]
--- Refresh all stats tables associated with a player dropdown
function Elements.player_dropdown.refresh_online()
for _, player_dropdown in Elements.player_dropdown:online_elements() do
local target_player = ElementsExtra.online_player_dropdown.get_selected(player_dropdown)
local data_table = Elements.player_dropdown.data[player_dropdown]
Elements.player_stats_table.refresh(data_table, target_player)
end
end
--- Container added to the left gui flow
Elements.container = Gui.define("player_stats/container")
:draw(function(def, parent)
local container = Gui.elements.container(parent)
local header = Gui.elements.header(container, { caption = { "exp-gui_player-stats.caption-main" } })
local data_table = Elements.player_stats_table(container)
Elements.player_dropdown(header, data_table)
return Gui.elements.container.get_root_element(container)
end)
--- Add the element to the left flow with a toolbar button
Gui.add_left_element(Elements.container, false)
Gui.toolbar.create_button{
name = "toggle_player_stats",
sprite = "item/power-armor-mk2",
tooltip = { "exp-gui_player-stats.tooltip-main" },
left_element = Elements.container,
visible = function(player, element)
return Roles.player_has_permission(player, "exp_scenario.gui.playerdata")
end
}
return {
elements = Elements,
on_nth_tick = {
[300] = Elements.player_dropdown.refresh_online
}
}