1
0
mirror of https://github.com/Refactorio/RedMew.git synced 2025-12-26 00:42:20 +02:00
Files
RedMew/features/gui/production_hud.lua
RedRafe 17799ffe65 Add Production HUD feature (#1497)
* Fix discord message

* Add production HUD

* Actually using correct input/output directions

* Add action feedbacks
2025-09-01 21:59:42 +02:00

339 lines
11 KiB
Lua

-- This feature adds a small HUD for production stats,
-- similar to Factorio built-in "P", but always on screen
-- made by RedRafe
-- ======================================================= --
local config = require 'config'.production_hud
local Event = require 'utils.event'
local Global = require 'utils.global'
local Gui = require 'utils.gui'
local math = require 'utils.math'
local table = require 'utils.table'
local math_abs = math.abs
local round_sig = math.round_sig
local Public = {}
local player_settings = {
--[player.index] = {
-- precision_index = defines.flow_precision_index.ten_minutes,
-- items = { 'iron-ore', 'copper-ore' },
--}
}
Global.register({
player_settings = player_settings,
}, function(tbl)
player_settings = tbl.player_settings
end)
local item_p = prototypes.item
local fluid_p = prototypes.fluid
local to_time = {
[defines.flow_precision_index.five_seconds] = '5s',
[defines.flow_precision_index.one_minute] = '1m',
[defines.flow_precision_index.ten_minutes] = '10m',
[defines.flow_precision_index.one_hour] = '1h',
[defines.flow_precision_index.ten_hours] = '10h',
[defines.flow_precision_index.fifty_hours] = '50h',
[defines.flow_precision_index.two_hundred_fifty_hours] = '250h',
[defines.flow_precision_index.one_thousand_hours] = '1000h',
}
local si_prefixes = {
{ 'Q', 1e30 }, -- quetta
{ 'R', 1e27 }, -- ronna
{ 'Y', 1e24 }, -- yotta
{ 'Z', 1e21 }, -- zetta
{ 'E', 1e18 }, -- exa
{ 'P', 1e15 }, -- peta
{ 'T', 1e12 }, -- tera
{ 'G', 1e09 }, -- giga
{ 'M', 1e06 }, -- mega
{ 'k', 1e03 }, -- kilo
}
---@param value number
---@return string
local function format_si(value)
if value == 0 then
return '0'
end
local abs_value = math_abs(round_sig(value, 3))
for i = #si_prefixes, 1, -1 do
local suffix = si_prefixes[i]
if abs_value >= suffix[2] then
local scaled = value / suffix[2]
return ('%.2f%s'):format(scaled, suffix[1])
end
end
if value > 100 then
return ('%d'):format(value)
elseif value > 10 then
return ('%.1f'):format(value)
end
return ('%.2f'):format(value)
end
-- == GUI =====================================================================
local main_frame_name = Gui.uid_name()
local main_button_name = Gui.uid_name()
local action_scroll_precision_index = Gui.uid_name()
local action_add_item = Gui.uid_name()
local action_remove_item = Gui.uid_name()
---@param parent LuaGuiElement
---@param items string[]
local function init_hud(parent, items)
Gui.clear(parent)
Public.sanitize_player_settings(parent.player_index)
--- Appending rows
for _, name in pairs(items) do
local flow = parent.add { type = 'flow', direction = 'horizontal', name = name }
flow.add {
type = 'sprite-button',
name = action_remove_item,
sprite = (item_p[name] and 'item.' or 'fluid.') .. name,
tooltip = {
'production_hud.item_tooltip',
{ '?', { 'item-name.' .. name }, { 'entity-name.' .. name }, { 'fluid-name.' .. name } },
},
tags = { name = name },
}
Gui.set_style(flow, { padding = 2, vertical_align = 'center' })
Gui.add_pusher(flow)
local vert = flow.add { type = 'flow', direction = 'vertical', name = 'stats' }
Gui.set_style(vert, { vertical_align = 'center', vertical_spacing = 0, horizontal_align = 'right' })
for _, info in pairs({
{ name = 'plus', font_color = { 150, 255, 150 }, caption = '---' },
{ name = 'minus', font_color = { 255, 150, 150 }, caption = '---' },
}) do
local l = vert.add {
type = 'label',
name = info.name,
caption = info.caption,
}
Gui.set_style(l, { font_color = info.font_color, minimal_width = 52, horizontal_align = 'right' })
end
end
end
---@param player_index uint
Public.sanitize_player_settings = function(player_index)
local s = player_settings[player_index]
if not (s and s.items) then
return
end
for i = #s.items, 1, -1 do
local name = s.items[i]
if item_p[name] == nil and fluid_p[name] == nil then
table.remove(s.items, i)
end
end
end
---@param player LuaPlayer
Public.toggle = function(event)
Public.toggle_main_button(event.player)
end
---@param player LuaPlayer
Public.toggle_main_button = function(player)
local frame = player.gui.screen[main_frame_name]
if frame and frame.valid then
frame.visible = not frame.visible
else
Public.get_main_frame(player)
end
end
---@param player LuaPlayer
Public.get_main_frame = function(player)
local frame = player.gui.screen[main_frame_name]
if frame and frame.valid then
return Public.update_main_frame(player)
end
local data = {}
local settings = player_settings[player.index]
frame = player.gui.screen.add {
type = 'frame',
name = main_frame_name,
direction = 'vertical',
}
Gui.set_style(frame, { padding = 4, width = 256 })
do -- header
local flow, label, button
flow = frame.add { type = 'flow', direction = 'horizontal' }
flow.drag_target = frame
label = flow.add({ type = 'label', style = 'subheader_caption_label', caption = 'Production' })
label.drag_target = frame
Gui.set_style(label, { left_padding = 0 })
Gui.add_pusher(flow).drag_target = frame
--- Display time
button = flow.add {
type = 'sprite-button',
name = action_scroll_precision_index,
caption = to_time[settings.precision_index],
style = 'frame_button',
}
data.action_scroll_precision_index = button
Gui.set_style(button, { height = 24, width = 48, font_color = { 255, 255, 255 } })
Gui.set_data(button, data)
--- Add new
button = flow.add {
type = 'choose-elem-button',
name = action_add_item,
style = 'frame_button',
tooltip = { 'production_hud.new_item_tooltip' },
elem_type = 'signal',
signal = { type = 'virtual', name = 'shape-cross' },
}
Gui.set_style(button, { size = 24, font_color = { 255, 255, 255 } })
Gui.set_data(button, data)
end
do -- body
local panel = frame.add { type = 'frame', style = 'quick_bar_inner_panel' }
local tbl = panel.add { type = 'table', column_count = 2, draw_horizontal_lines = true }
data.table = tbl
end
Gui.set_data(frame, data)
Public.update_main_frame(player)
frame.force_auto_center()
end
---@param player LuaPlayer
Public.update_main_frame = function(player)
local frame = player.gui.screen[main_frame_name]
if not (frame and frame.valid and frame.visible) then
return
end
local data = Gui.get_data(frame)
local tbl = data.table
local settings = player_settings[player.index]
if #settings.items ~= table_size(tbl.children) then
init_hud(tbl, settings.items)
end
local item_stats = player.force.get_item_production_statistics(player.physical_surface)
local fluid_stats = player.force.get_fluid_production_statistics(player.physical_surface)
for _, name in pairs(settings.items) do
local children = tbl[name]
local stats = ((item_p[name] ~= nil) and item_stats or fluid_stats)
local minus = stats.get_flow_count { name = name, category = 'output', precision_index = settings.precision_index }
local plus = stats.get_flow_count { name = name, category = 'input', precision_index = settings.precision_index }
local count = stats.get_input_count(name) - stats.get_output_count(name)
children.stats.plus.caption = format_si(plus)
children.stats.minus.caption = format_si(minus)
children[action_remove_item].number = count
end
end
Gui.allow_player_to_toggle_top_element_visibility(main_button_name)
Gui.on_click(main_button_name, Public.toggle)
Gui.on_click(action_scroll_precision_index, function(event)
local settings = player_settings[event.player_index]
settings.precision_index = (settings.precision_index + 1) % table_size(defines.flow_precision_index)
local data = Gui.get_data(event.element)
data.action_scroll_precision_index.caption = to_time[settings.precision_index]
Public.update_main_frame(event.player)
end)
Gui.on_elem_changed(action_add_item, function(event)
local player = event.player
local element = event.element
local items = player_settings[player.index].items
local item = element.elem_value and element.elem_value.name
element.elem_value = { name = 'shape-cross', type = 'virtual' }
if not item then
player.print({ 'production_hud.err_invalid_item' }, { sound_path = 'utility/cannot_build' })
return
end
if item_p[item] == nil and fluid_p[item] == nil then
player.print({ 'production_hud.err_invalid_item' }, { sound_path = 'utility/cannot_build' })
return
end
if table.contains(items, item) then
player.print({ 'production_hud.err_item_already_present' }, { sound_path = 'utility/cannot_build' })
return
end
if #items >= config.limit then
player.print({ 'production_hud.err_limit_reached', config.limit }, { sound_path = 'utility/cannot_build' })
return
end
table.insert(items, item)
player.play_sound{ path = 'utility/armor_insert' }
Public.update_main_frame(player)
end)
Gui.on_click(action_remove_item, function(event)
if event.button ~= defines.mouse_button_type.right then
return
end
table.remove_element(player_settings[event.player_index].items, event.element.tags.name)
event.player.play_sound{ path = 'utility/armor_remove' }
Public.update_main_frame(event.player)
end)
-- == EVENTS ==================================================================
Event.add(defines.events.on_player_created, function(event)
local player = game.get_player(event.player_index)
if not (player and player.valid) then
return
end
Gui.add_top_element(player, {
name = main_button_name,
type = 'sprite-button',
sprite = 'utility/side_menu_production_icon',
tooltip = { 'production_hud.feature_tooltip' },
})
player_settings[player.index] = {
precision_index = defines.flow_precision_index.ten_minutes,
items = table.deepcopy(config.starting_items or {}),
}
end)
Event.on_nth_tick(307, function()
for _, p in pairs(game.connected_players) do
Public.update_main_frame(p)
end
end)
Event.on_configuration_changed(function()
for _, p in pairs(game.players) do
Public.sanitize_player_settings(p.index)
end
end)
-- ============================================================================