Files
factorio-scenario-ExpCluster/exp_scenario/module/gui/research_milestones.lua
Cooldude2606 7ab721b4b6 Refactor some of the Guis from the legacy plugin (#399)
* Fix bugs in core and add default args to Gui defs

* Refactor production Gui

* Refactor landfill blueprint button

* Fix more bugs in core

* Consistent naming of new guis

* Refactor module inserter gui

* Refactor surveillance gui

* Add shorthand for data from arguments

* Make element names consistent

* Add types

* Change how table rows work

* Refactor player stats gui

* Refactor quick actions gui

* Refactor research milestones gui

* Refactor player bonus gui

* Refactor science production gui

* Refactor autofill gui

* Cleanup use of aligned flow

* Rename "Gui.element" to "Gui.define"

* Rename Gui types

* Rename property_from_arg

* Add guide for making guis

* Add full reference document

* Add condensed reference

* Apply style guide to refactored guis

* Bug fixes
2025-08-29 14:30:30 +01:00

403 lines
16 KiB
Lua

--[[-- Gui - Research Milestones
Adds a gui for tracking research milestones
]]
local ExpUtil = require("modules/exp_util")
local Gui = require("modules/exp_gui")
local Roles = require("modules/exp_legacy/expcore/roles")
local config = require("modules/exp_legacy/config/research")
local table_to_json = helpers.table_to_json
local write_file = helpers.write_file
local string_format = string.format
local display_size = 8
--- @class ExpGui_ResearchMilestones.elements
local Elements = {}
local research_time_format = ExpUtil.format_time_factory{ format = "clock", hours = true, minutes = true, seconds = true }
local research_time_format_nil = research_time_format(nil)
local font_color = {
neutral = { r = 1, g = 1, b = 1 },
positive = { r = 0.3, g = 1, b = 0.3 },
negative = { r = 1, g = 0.3, b = 0.3 },
}
--- @class ExpGui_ResearchMilestones.research_targets
--- @field index_lookup table<string, number>
--- @field target_times table<number, { name: string, target: number, label: LocalisedString }>
local research_targets = {
index_lookup = {},
target_times = {},
max_start_index = 0,
length = 0,
}
--- Select the mod set to be used for milestones
for _, mod_name in ipairs(config.mod_set_lookup) do
if script.active_mods[mod_name] then
config.mod_set = mod_name
break
end
end
do --- Calculate the research targets
local research_index = 1
local total_time = 0
for name, time in pairs(config.milestone[config.mod_set]) do
research_targets.index_lookup[name] = research_index
total_time = total_time + time * 60
research_targets.target_times[research_index] = {
name = name,
target = total_time,
label = research_time_format(total_time),
}
research_index = research_index + 1
end
research_targets.length = research_index - 1
research_targets.max_start_index = math.max(1, research_index - display_size)
end
--- Display label for the clock display
--- @class ExpGui_ResearchMilestones.elements.clock_label: ExpElement
--- @overload fun(parent: LuaGuiElement): LuaGuiElement
Elements.clock_label = Gui.define("research_milestones/clock_label")
:track_all_elements()
:draw{
type = "label",
caption = research_time_format_nil,
style = "heading_2_label",
} --[[ @as any ]]
--- Update the clock label for all online players
function Elements.clock_label.refresh_online()
local current_time = research_time_format(game.tick)
for _, clock_label in Elements.clock_label:online_elements() do
clock_label.caption = current_time
end
end
--- Label used for all parts of the table
--- @class ExpGui_ResearchMilestones.elements.milestone_table_label: ExpElement
--- @overload fun(parent: LuaGuiElement, caption: LocalisedString?, minimal_width: number?, horizontal_align: string?): LuaGuiElement
Elements.milestone_table_label = Gui.define("research_milestones/table_label")
:draw{
type = "label",
caption = Gui.from_argument(1),
style = "heading_2_label",
}
:style{
minimal_width = Gui.from_argument(2, 70),
horizontal_align = Gui.from_argument(3, "right"),
font_color = font_color.neutral,
} --[[ @as any ]]
--- @class ExpGui_ResearchMilestones.elements.milestone_table.row_elements
--- @field name LuaGuiElement
--- @field target LuaGuiElement
--- @field achieved LuaGuiElement
--- @field difference LuaGuiElement
--- @class ExpGui_ResearchMilestones.elements.milestone_table.row_data
--- @field name LocalisedString
--- @field target LocalisedString
--- @field achieved LocalisedString
--- @field difference LocalisedString
--- @field color Color
--- A table containing all of the current researches and their times / targets
--- @class ExpGui_ResearchMilestones.elements.milestone_table: ExpElement
--- @field data table<LuaGuiElement, ExpGui_ResearchMilestones.elements.milestone_table.row_elements[]>
--- @overload fun(parent: LuaGuiElement): LuaGuiElement
Elements.milestone_table = Gui.define("research_milestones/milestone_table")
:track_all_elements()
:draw(function(_, parent)
local milestone_table = Gui.elements.scroll_table(parent, 390, 4)
Elements.milestone_table_label(milestone_table, { "exp-gui_research-milestones.caption-name" }, 180, "left")
Elements.milestone_table_label(milestone_table, { "exp-gui_research-milestones.caption-target" })
Elements.milestone_table_label(milestone_table, { "exp-gui_research-milestones.caption-achieved" })
Elements.milestone_table_label(milestone_table, { "exp-gui_research-milestones.caption-difference" })
return milestone_table
end)
:element_data{} --[[ @as any ]]
do local _row_data = {}
--- @type ExpGui_ResearchMilestones.elements.milestone_table.row_data
local empty_row_data = { color = font_color.positive }
--- Get the row data for a force and research
--- @param force LuaForce
--- @param research_index number
function Elements.milestone_table._clear_row_data_cache(force, research_index)
local row_key = string_format("%s:%s", force.name, research_index)
_row_data[row_key] = nil
end
--- Get the row data for a force and research
--- @param force LuaForce
--- @param research_index number
--- @return ExpGui_ResearchMilestones.elements.milestone_table.row_data
function Elements.milestone_table.calculate_row_data(force, research_index)
local row_key = string_format("%s:%s", force.name, research_index)
return _row_data[row_key] or Elements.milestone_table._calculate_row_data(force, research_index)
end
--- Calculate the row data for a force and research
--- @param force LuaForce
--- @param research_index number
--- @return ExpGui_ResearchMilestones.elements.milestone_table.row_data
function Elements.milestone_table._calculate_row_data(force, research_index)
local row_key = string_format("%s:%s", force.name, research_index)
-- If there is no target entry then return empty row data
local entry = research_targets.target_times[research_index]
if not entry then
_row_data[row_key] = empty_row_data
return empty_row_data
end
-- Otherwise calculate the row data
assert(prototypes.technology[entry.name], "Invalid Research: " .. tostring(entry.name))
local row_data = {} --- @cast row_data ExpGui_ResearchMilestones.elements.milestone_table.row_data
row_data.name = { "exp-gui_research-milestones.caption-research-name", entry.name, prototypes.technology[entry.name].localised_name }
row_data.target = entry.label
local time = Elements.container.get_achieved_time(force, research_index)
if not time then
row_data.achieved = research_time_format_nil
row_data.difference = research_time_format_nil
row_data.color = font_color.neutral
else
row_data.achieved = research_time_format(time)
local diff = time - entry.target
row_data.difference = (diff < 0 and "-" or "+") .. research_time_format(math.abs(diff))
row_data.color = (diff < 0 and font_color.positive) or font_color.negative
end
-- Store it in the cache for faster access next time
_row_data[row_key] = row_data
return row_data
end
end
--- Adds a row to the milestone table
--- @param milestone_table LuaGuiElement
--- @param row_data ExpGui_ResearchMilestones.elements.milestone_table.row_data
function Elements.milestone_table.add_row(milestone_table, row_data)
local rows = Elements.milestone_table.data[milestone_table]
rows[#rows + 1] = {
name = Elements.milestone_table_label(milestone_table, row_data.name, 180, "left"),
target = Elements.milestone_table_label(milestone_table, row_data.target),
achieved = Elements.milestone_table_label(milestone_table, row_data.achieved),
difference = Elements.milestone_table_label(milestone_table, row_data.difference),
}
end
--- Update a row to match the given data
--- @param milestone_table LuaGuiElement
--- @param row_index number
--- @param row_data ExpGui_ResearchMilestones.elements.milestone_table.row_data
function Elements.milestone_table.refresh_row(milestone_table, row_index, row_data)
local row = Elements.milestone_table.data[milestone_table][row_index]
row.name.caption = row_data.name
row.target.caption = row_data.target
row.achieved.caption = row_data.achieved
row.difference.caption = row_data.difference
row.difference.style.font_color = row_data.color
end
--- Update a row to match the given data for all players on a force
--- @param force LuaForce
--- @param row_index number
--- @param row_data ExpGui_ResearchMilestones.elements.milestone_table.row_data
function Elements.milestone_table.refresh_force_online_row(force, row_index, row_data)
for _, milestone_table in Elements.milestone_table:online_elements(force) do
Elements.milestone_table.refresh_row(milestone_table, row_index, row_data)
end
end
--- Refresh all the labels on the table
--- @param milestone_table LuaGuiElement
function Elements.milestone_table.refresh(milestone_table)
local force = Gui.get_player(milestone_table).force --[[ @as LuaForce ]]
local start_index = Elements.container.calculate_starting_research_index(force)
for row_index = 1, display_size do
local row_data = Elements.milestone_table.calculate_row_data(force, start_index + row_index - 1)
Elements.milestone_table.refresh_row(milestone_table, row_index, row_data)
end
end
--- Refresh all tables for a player
function Elements.milestone_table.refresh_player(player)
local force = player.force --[[ @as LuaForce ]]
local start_index = Elements.container.calculate_starting_research_index(force)
for _, milestone_table in Elements.milestone_table:online_elements(player) do
for row_index = 1, display_size do
local row_data = Elements.milestone_table.calculate_row_data(force, start_index + row_index - 1)
Elements.milestone_table.refresh_row(milestone_table, row_index, row_data)
end
end
end
--- Refresh all tables for online players on a force
function Elements.milestone_table.refresh_force_online(force)
local row_data = {}
local start_index = Elements.container.calculate_starting_research_index(force)
for row_index = 1, display_size do
row_data[row_index] = Elements.milestone_table.calculate_row_data(force, start_index + row_index - 1)
end
for _, milestone_table in Elements.milestone_table:online_elements(force) do
for row_index = 1, display_size do
Elements.milestone_table.refresh_row(milestone_table, row_index, row_data[row_index])
end
end
end
--- Container added to the left gui flow
--- @class ExpGui_ResearchMilestones.elements.container: ExpElement
--- @field data table<LuaForce, number[]>
Elements.container = Gui.define("research_milestones/container")
:draw(function(def, parent)
local container = Gui.elements.container(parent)
local header = Gui.elements.header(container, { caption = { "exp-gui_research-milestones.caption-main" } })
local milestone_table = Elements.milestone_table(container)
Elements.clock_label(header)
local force = Gui.get_player(parent).force --[[ @as LuaForce ]]
local start_index = Elements.container.calculate_starting_research_index(force)
for research_index = start_index, start_index + display_size - 1 do
local row_data = Elements.milestone_table.calculate_row_data(force, research_index)
Elements.milestone_table.add_row(milestone_table, row_data)
end
return Gui.elements.container.get_root_element(container)
end)
:force_data{} --[[ @as any ]]
--- Set the achieved time for a force
--- @param force LuaForce
--- @param research_index number
--- @param time number
function Elements.container.set_achieved_time(force, research_index, time)
Elements.milestone_table._clear_row_data_cache(force, research_index)
Elements.container.data[force][research_index] = time
end
--- Get the achieved time for a force
--- @param force LuaForce
--- @param research_index number
--- @return number
function Elements.container.get_achieved_time(force, research_index)
return Elements.container.data[force][research_index]
end
--- Calculate the starting research index for a force
--- @param force LuaForce
--- @return number
function Elements.container.calculate_starting_research_index(force)
local force_data = Elements.container.data[force]
local research_index = research_targets.length
-- # does not work here because it returned the array alloc size
for i = 1, research_targets.length do
if not force_data[i] then
research_index = i
break
end
end
return math.clamp(research_index - 2, 1, research_targets.max_start_index)
end
--- Append all research times to the research log
--- @param force LuaForce
function Elements.container.append_log_line(force)
local result_data = {}
local force_data = Elements.container.data[force]
for name, research_index in pairs(research_targets.index_lookup) do
result_data[name] = force_data[research_index]
end
write_file(config.file_name, table_to_json(result_data) .. "\n", true, 0)
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_research_milestones",
left_element = Elements.container,
sprite = "item/space-science-pack",
tooltip = { "exp-gui_research-milestones.tooltip-main" },
visible = function(player, element)
return Roles.player_allowed(player, "gui/research")
end
}
--- @param event EventData.on_research_finished
local function on_research_finished(event)
local research_name = event.research.name
local research_level = event.research.level
local force = event.research.force
-- Check if the log should be updated and print a message to chat
if config.inf_res[config.mod_set][research_name] then
local log_requirement = config.bonus_inventory.log[config.mod_set]
if research_name == log_requirement.name and research_level == log_requirement.level + 1 then
Elements.container.append_log_line(force)
end
if not (event.by_script) then
game.print{ "exp-gui_research-milestones.notice-inf", research_time_format(game.tick), research_name, research_level - 1 }
end
elseif not (event.by_script) then
game.print{ "exp-gui_research-milestones.notice", research_time_format(game.tick), research_name }
end
-- If the research does not have a milestone we don't need to update the gui
local research_index = research_targets.index_lookup[research_name]
if not research_index then
return
end
-- Calculate the various difference indexes
local previous_start_index = Elements.container.calculate_starting_research_index(force)
Elements.container.set_achieved_time(force, research_index, event.tick)
local start_index = Elements.container.calculate_starting_research_index(force)
if start_index == previous_start_index then
-- No change in start index so only need to update one row
local row_index = research_index - start_index + 1
if row_index > 0 and row_index <= 8 then
local row_data = Elements.milestone_table.calculate_row_data(force, research_index)
Elements.milestone_table.refresh_force_online_row(force, row_index, row_data)
end
else
-- Start index changed so we need to refresh the table
Elements.milestone_table.refresh_force_online(force)
end
end
--- Force a refresh of the research table when a player joins or changes force
--- @param event EventData.on_player_joined_game | EventData.on_player_changed_force
local function refresh_for_player(event)
Elements.milestone_table.refresh_player(Gui.get_player(event))
end
local e = defines.events
return {
elements = Elements,
events = {
[e.on_research_finished] = on_research_finished,
[e.on_player_joined_game] = refresh_for_player,
[e.on_player_changed_force] = refresh_for_player,
},
on_nth_tick = {
[60] = Elements.clock_label.refresh_online,
}
}