1
0
mirror of https://github.com/Refactorio/RedMew.git synced 2025-12-28 00:44:48 +02:00
Files
RedMew/features/gui/calculator/technology.lua
RedRafe 00f60ea7db Add calculator/technology feature (#1500)
* Add calculator/technology feature

* Update file header

* Updates

* Updates
2025-09-17 10:24:59 +02:00

454 lines
15 KiB
Lua

-- This feature adds a command "/calculator-technology" that shows a small popup
-- with the breakdown cost to research target technology.
-- made by RedRafe
-- ======================================================= --
local Command = require 'utils.command'
local Gui = require 'utils.gui'
local Ranks = require 'resources.ranks'
local Global = require 'utils.global'
local History = require 'utils.history'
local history = {}
local toggled = {}
Global.register({
history = history,
toggled = toggled,
}, function(tbl)
history = tbl.history
toggled = tbl.toggled
end)
local function get_history(player_index)
local h = history[player_index]
if not h then
h = History.new()
history[player_index] = h
end
return h
end
local function set_history(player_index, technology_name)
local h = get_history(player_index)
if technology_name == nil then
h:clear()
elseif prototypes.technology[technology_name] ~= nil then
h:add(technology_name)
end
end
local function dict_to_array(dict)
local array = {}
for k, v in pairs(dict) do
table.insert(array, { name = k, count = v })
end
return array
end
local function safe_div(a, b)
return b <= 0 and 0 or a / b
end
local function sort_lab_input(a, b)
local a_order = prototypes.item[a.name].order
local b_order = prototypes.item[b.name].order
return a_order < b_order
end
---@param force LuaForce
---@param technology_name string
local function get_research_info(force, technology_name)
if (technology_name == nil) or (force.technologies[technology_name] == nil) then
return {
cost_breakdown = {},
research_path = {},
}
end
local tech = force.technologies[technology_name]
local cost_breakdown = {
-- sci pack name/icon
-- relative count
-- absolute count
-- progress
}
local research_path = {
-- technology name/icon
-- ingredients w/ count,
-- order ?
-- localised_name
}
local relative = {}
local absolute = {}
local seen = {}
---@param technology LuaTechnology
local function compute_cost(technology)
if seen[technology.name] then
return
end
seen[technology.name] = true
local ingredients = {}
local absolute_cost = technology.research_unit_count or 0
local relative_cost = technology.researched and 0 or absolute_cost
for _, ingredient in pairs(technology.research_unit_ingredients) do
ingredients[ingredient.name] = absolute_cost * ingredient.amount
absolute[ingredient.name] = (absolute[ingredient.name] or 0) + absolute_cost * ingredient.amount
relative[ingredient.name] = (relative[ingredient.name] or 0) + relative_cost * ingredient.amount
end
ingredients = dict_to_array(ingredients)
table.sort(ingredients, sort_lab_input)
if not technology.researched then
table.insert(research_path, {
name = technology.name,
localised_name = technology.localised_name,
ingredients = ingredients,
})
end
for _, prereq in pairs(technology.prerequisites) do
compute_cost(prereq)
end
end
compute_cost(tech)
local tot_abs, tot_rel = 0, 0
for ingredient, count in pairs(absolute) do
local c = {
name = ingredient,
relative = relative[ingredient],
absolute = count,
progress = 1 - safe_div(relative[ingredient], count),
localised_name = prototypes.item[ingredient].localised_name,
}
table.insert(cost_breakdown, c)
tot_abs = tot_abs + c.absolute
tot_rel = tot_rel + c.relative
end
table.sort(cost_breakdown, sort_lab_input)
table.sort(research_path, function(a, b)
return a.name < b.name
end)
return {
research_path = research_path,
cost_breakdown = cost_breakdown,
average = {
absolute = tot_abs,
relative = tot_rel,
progress = 1 - safe_div(tot_rel, tot_abs),
},
}
end
-- == GUI =====================================================================
local main_frame_name = Gui.uid_name()
local close_button_name = Gui.uid_name()
local history_back_button_name = Gui.uid_name()
local history_forward_button_name = Gui.uid_name()
local select_button_name = Gui.uid_name()
local toggle_list_button_name = Gui.uid_name()
local shortcut_button_name = Gui.uid_name()
local on_technologies, off_technologies = '▼ Technologies', '▲ Technologies'
local function get_main_frame(player)
local frame = player.gui.screen[main_frame_name]
if frame and frame.valid then
Gui.clear(frame)
return frame
end
frame = player.gui.screen.add {
type = 'frame',
name = main_frame_name,
direction = 'vertical',
style = 'frame',
}
Gui.set_style(frame, {
horizontally_stretchable = true,
vertically_stretchable = true,
maximal_height = 600,
natural_width = 482,
top_padding = 8,
bottom_padding = 8,
})
Gui.set_data(frame, { frame = frame })
frame.force_auto_center()
player.opened = frame
return frame
end
local function show_ingredients(parent, ingredients)
local flow = parent.add { type = 'flow', direction = 'horizontal' }
Gui.set_style(flow, { horizontally_stretchable = true })
for _, ingredient in pairs(ingredients) do
local b = flow.add {
type = 'sprite-button',
style = 'slot',
number = ingredient.count,
sprite = 'item/' .. ingredient.name,
tooltip = prototypes.item[ingredient.name].localised_name,
}
Gui.set_style(b, { size = 32, font = 'default-small-semibold' })
end
end
local function shortcut_button(parent, info)
local b = parent
.add { type = 'flow' }
.add {
type = 'sprite-button',
style = 'transparent_slot',
sprite = 'technology/' .. info.name,
name = shortcut_button_name,
tags = { name = info.name },
tooltip = {'', '[color=0.5,0.8,0.94][font=var]Click[/font][/color] to go to this technology\'s breakdown'}
}
Gui.set_style(b, { size = 32 })
return b
end
local function progressbar(parent, info)
local p = parent.add {
type = 'progressbar',
value = info.progress,
tooltip = ('%.2f %%'):format(info.progress * 100),
style = 'achievement_progressbar',
}
Gui.set_style(p, { width = 80 })
return p
end
local function draw(player)
local frame = get_main_frame(player)
local data = Gui.get_data(frame)
local h = get_history(player.index)
local technology_name = h:get()
local info = get_research_info(player.force, technology_name)
do --- title
local flow = frame.add { type = 'flow', direction = 'horizontal' }
Gui.set_style(flow, { horizontal_spacing = 8, vertical_align = 'center', bottom_padding = 4 })
local label = flow.add { type = 'label', caption = 'Research calculator', style = 'frame_title' }
label.drag_target = frame
Gui.add_dragger(flow, frame)
local history_flow = flow.add { type = 'flow', direction = 'horizontal' }
Gui.set_style(history_flow, { horizontal_spacing = 0, padding = 0 })
local backward = history_flow.add {
type = 'sprite-button',
name = history_back_button_name,
sprite = 'utility/backward_arrow',
clicked_sprite = 'utility/backward_arrow_black',
style = 'close_button',
tooltip = 'Back',
}
backward.enabled = h:peek_previous() ~= nil
local forward = history_flow.add {
type = 'sprite-button',
name = history_forward_button_name,
sprite = 'utility/forward_arrow',
clicked_sprite = 'utility/forward_arrow_black',
style = 'close_button',
tooltip = 'Forward',
}
forward.enabled = h:peek_next() ~= nil
local close_button = flow.add {
type = 'sprite-button',
name = close_button_name,
sprite = 'utility/close',
clicked_sprite = 'utility/close_black',
style = 'close_button',
tooltip = { 'gui.close-instruction' },
}
Gui.set_data(close_button, { frame = frame })
end
do --- body
local body = frame.add { type = 'frame', style = 'inside_shallow_frame_packed', direction = 'vertical' }.add { type = 'scroll-pane', style = 'naked_scroll_pane' }
Gui.set_style(body.parent, { horizontally_stretchable = true, top_padding = 8, left_padding = 8, bottom_padding = 8 })
Gui.set_style(body, { right_padding = 8 })
do --- Technology selection
local flow = body.add { type = 'frame', style = 'bordered_frame' }.add { type = 'flow', direction = 'horizontal' }
flow.add { type = 'label', caption = 'Select technology', style = 'caption_label' }
Gui.add_pusher(flow, 'horizontal')
local select_button = flow.add {
type = 'choose-elem-button',
name = select_button_name,
style = 'slot_button_in_shallow_frame',
elem_type = 'technology',
technology = technology_name,
}
data.select_button = select_button
Gui.set_data(select_button, data)
end
do --- Technology cost breakdown
local breakdown = body.add {
type = 'table',
style = 'finished_game_table',
column_count = 4,
draw_horizontal_line_after_headers = false,
}
data.breakdown = breakdown
breakdown.style.column_alignments[2] = 'center'
breakdown.style.column_alignments[3] = 'right'
breakdown.style.column_alignments[4] = 'right'
Gui.set_style(breakdown, { horizontally_stretchable = true })
for _, title in pairs({
'Science',
'Progress',
'Relative',
'Absolute',
}) do
breakdown.add { type = 'label', caption = title, style = 'bold_label' }
end
for _, c in pairs(info.cost_breakdown) do
breakdown.add { type = 'label', caption = { '', '[img=item/' .. c.name .. '] ', c.localised_name } }
progressbar(breakdown, c)
breakdown.add { type = 'label', caption = c.relative }
breakdown.add { type = 'label', caption = c.absolute }
end
local avg = info.average
if avg then
breakdown.add { type = 'label', caption = ('Completion %d %%'):format(avg.progress * 100), style = 'info_label' }
progressbar(breakdown, avg)
breakdown.add { type = 'label', caption = avg.relative }
breakdown.add { type = 'label', caption = avg.absolute }
end
breakdown.visible = (technology_name ~= nil)
end
do --- Technology research path
local label = body.add {
type = 'label',
style = 'bold_label',
caption = toggled[player.index] and on_technologies or off_technologies,
name = toggle_list_button_name,
tooltip = 'Hide/Show technologies list',
}
data.label = label
Gui.set_data(label, data)
local deep = body.add { type = 'frame', style = 'deep_frame_in_shallow_frame_for_description', direction = 'vertical' }
Gui.set_style(deep, { padding = 0, minimal_height = 4 })
local list = deep.add { type = 'scroll-pane', vertical_scroll_policy = 'never' }.add { type = 'table', style = 'table_with_selection', column_count = 3 }
Gui.set_style(list.parent, { horizontally_squashable = false })
list.visible = toggled[player.index] or false
data.list = list
Gui.set_data(list, data)
for _, t in pairs(info.research_path) do
shortcut_button(list, t)
list.add { type = 'label', caption = t.localised_name }
show_ingredients(list, t.ingredients)
end
end
end
end
Gui.on_click(close_button_name, function(event)
Gui.destroy(Gui.get_data(event.element).frame)
end)
Gui.on_custom_close(main_frame_name, function(event)
Gui.destroy(event.element)
end)
Gui.on_click(history_back_button_name, function(event)
local h = get_history(event.player_index)
h:previous()
draw(event.player)
end)
Gui.on_click(history_forward_button_name, function(event)
local h = get_history(event.player_index)
h:next()
draw(event.player)
end)
Gui.on_elem_changed(select_button_name, function(event)
set_history(event.player_index, event.element.elem_value)
draw(event.player)
end)
Gui.on_click(toggle_list_button_name, function(event)
local data = Gui.get_data(event.element)
data.list.visible = not data.list.visible
toggled[event.player_index] = data.list.visible
data.label.caption = data.list.visible and on_technologies or off_technologies
end)
Gui.on_click(shortcut_button_name, function(event)
if get_history(event.player_index):get() == event.element.tags.name then
return
end
set_history(event.player_index, event.element.tags.name)
draw(event.player)
end)
-- == COMMANDS ================================================================
Command.add('calculator-technology', {
description = 'Computes the cost in science packs to research target technology',
arguments = { 'technology' },
default_values = { technology = '' },
allowed_by_server = false,
required_rank = Ranks.guest,
capture_excess_arguments = true,
}, function(arguments, player, _)
set_history(player.index, arguments.technology)
draw(player)
end)
Command.add('calculator-technology-for-player', {
description = 'Computes the cost in science packs to research target technology for target player',
arguments = { 'technology', 'player' },
allowed_by_server = false,
required_rank = Ranks.admin,
capture_excess_arguments = true,
}, function(arguments, player, _)
local target_player = game.get_player(arguments.player)
if not player then
player.print('Invalid <player>')
return
end
if prototypes.technology[arguments.technology] == nil then
player.print('Invalid <technology>')
return
end
set_history(player.index, arguments.technology)
draw(player)
if player ~= target_player then
set_history(target_player.index, arguments.technology)
draw(target_player)
end
end)