local flib_dictionary = require("__flib__/dictionary-lite") local flib_format = require("__flib__/format") local flib_gui = require("__flib__/gui-lite") local flib_math = require("__flib__/math") local flib_table = require("__flib__/table") local gui_util = require("__RateCalculator__/scripts/gui-util") --- @alias DisplayCategory --- | "products" --- | "intermediates" --- | "ingredients" --- @alias GenericPrototype LuaEntityPrototype|LuaFluidPrototype|LuaItemPrototype --- @class RatesDisplayData: Rates --- @field category DisplayCategory --- @field path SpritePath --- @field sorting_rate double --- @field completed boolean --- @field is_watts boolean --- @alias CategoryDisplayData table --- @alias DisplayDataLookup table --- @class RATESTEMP --- @field path SpritePath --- @field icon SpritePath --- @field temperature double? --- @field machines_caption LocalisedString --- @field rate_caption LocalisedString --- @field rate_color string --- @field rate_suffix LocalisedString? --- @field rate double --- @field completed boolean local colors = { green = "150,255,150", red = "255,150,150", white = "255,255,255", } --- @param amount number --- @param prefer_si boolean --- @param positive_prefix boolean --- @return string local function format_number(amount, prefer_si, positive_prefix) local formatted = "" if prefer_si or math.abs(amount) >= 10000 then formatted = flib_format.number(amount, true) else local precision = 0.001 if math.abs(amount) >= 1000 then precision = 1 elseif math.abs(amount) >= 100 then precision = 0.1 elseif math.abs(amount) >= 10 then precision = 0.01 end formatted = flib_format.number(flib_math.round(amount, precision)) end if positive_prefix and amount > 0 then formatted = "+" .. formatted end return formatted end --- @param counts MachineCounts --- @param include_numbers boolean --- @return string local function build_machine_icons(counts, include_numbers) local output = "" for name, count in pairs(counts) do output = output .. "[entity=" .. name .. "] " if include_numbers then output = output .. count .. " " end end return output end --- @param rate Rate --- @param color string --- @param label LocalisedString --- @param suffix LocalisedString local function build_rate_tooltip(rate, color, label, suffix) return { "gui.rcalc-colored-caption", { "", { "gui.rcalc-tooltip-entry", label, { "", format_number(rate.rate, false, false), suffix }, }, { "gui.rcalc-parenthesized-caption", { "gui.rcalc-machines-caption", { "", format_number(rate.rate / rate.machines, false, false), suffix }, { "gui.rcalc-caption-with-suffix", format_number(rate.machines, false, false), { "gui.rcalc-machines" }, }, }, }, }, color, } end --- @param num number --- @return string local function get_net_color(num) if num > 0 then return colors.green elseif num < 0 then return colors.red else return colors.white end end --- @param e EventData.on_gui_click local function on_completion_checkbox_checked(e) local self = global.gui[e.player_index] if not self then return end local set = self.sets[self.selected_set_index] if set then set.completed[e.element.name] = e.element.state or nil end end --- @param e EventData.on_gui_click local function on_rates_flow_clicked(e) if not remote.interfaces["RecipeBook"] or not e.alt then return end local sprite = e.element.icon.sprite local type, name = string.match(sprite, "(.*)/(.*)") if not type or not name then return end remote.call("RecipeBook", "open_page", e.player_index, type, name) end --- @param e EventData.on_gui_hover local function on_rates_flow_hovered(e) local self = global.gui[e.player_index] if not self or not self.elems.rcalc_window.valid then return end local elem = e.element local path = elem.name local data = self.display_data_lookup[path] if not data then return end local category = data.category local output = data.output local input = data.input local is_watts = data.is_watts local suffix = is_watts and { "si-unit-symbol-watt" } or { "gui.rcalc-timescale-suffix-" .. self.selected_timescale } --- @type Rate local category_rate = category == "ingredients" and input or output --- @type GenericPrototype local prototype = game[data.type .. "_prototypes"][data.name] local name = prototype.localised_name if data.temperature then name = { "", name, " (", { "format-degrees-c-compact", format_number(data.temperature, false, false) }, ")", } end local machine_counts_caption = build_machine_icons(category_rate.machine_counts, true) --- @type LocalisedString local intermediate_breakdown_caption = { "" } if category == "intermediates" then machine_counts_caption = machine_counts_caption .. "→ " .. build_machine_icons(input.machine_counts, true) local net_rate = output.rate - input.rate rate_caption = { "gui.rcalc-colored-caption", { "", { "gui.rcalc-tooltip-entry", { "gui.rcalc-net-rate" }, { "", format_number(net_rate, false, true), suffix }, }, { "gui.rcalc-parenthesized-caption", { "gui.rcalc-caption-with-suffix", format_number(net_rate / (output.rate / output.machines), false, true), { "gui.rcalc-machines" }, }, }, }, get_net_color(net_rate), } intermediate_breakdown_caption = { "", "\n\n", build_rate_tooltip(output, colors.green, { "gui.rcalc-production" }, suffix), "\n", build_rate_tooltip(input, colors.red, { "gui.rcalc-consumption" }, suffix), } else rate_caption = build_rate_tooltip(category_rate, colors.white, { "gui.rcalc-rate" }, suffix) end machine_counts_caption = machine_counts_caption .. "\n" elem.tooltip = { "", { "gui.rcalc-tooltip-title", name }, machine_counts_caption, rate_caption, intermediate_breakdown_caption, remote.interfaces["RecipeBook"] and { "", "\n\n", { "gui.rcalc-open-in-recipe-book-instruction", { "mod-name.RecipeBook" } } } or nil, } end --- @param e EventData.on_gui_leave local function on_rates_flow_left(e) e.element.tooltip = "" end flib_gui.add_handlers({ on_completion_checkbox_checked = on_completion_checkbox_checked, on_rates_flow_clicked = on_rates_flow_clicked, on_rates_flow_hovered = on_rates_flow_hovered, on_rates_flow_left = on_rates_flow_left, }) --- @param parent LuaGuiElement --- @param category DisplayCategory --- @param rates RatesDisplayData[] --- @param show_machines boolean --- @param show_checkboxes boolean --- @param show_breakdown boolean local function build_rates_table(parent, category, rates, show_machines, show_checkboxes, show_breakdown) --- @type GuiElemDef local rates_table = { type = "table", style = "slot_table", column_count = 1 } for _, data in pairs(rates) do local output = data.output local input = data.input local category_rate = data.category == "ingredients" and input or output local raw_rate = category_rate.rate local machines = format_number(category_rate.machines, false, false) local machines_caption = { "gui.rcalc-machines-caption", build_machine_icons(category_rate.machine_counts, false), machines, } local rate_color = colors.white if category == "intermediates" then raw_rate = flib_math.round(output.rate - input.rate, 0.00001) -- Floating point sucks rate_color = get_net_color(raw_rate) local net_machines = raw_rate / (output.rate / output.machines) machines_caption = { "gui.rcalc-net-machines-caption", machines_caption, rate_color, format_number(net_machines, false, true), } end local rate_caption = format_number(raw_rate, data.is_watts, category == "intermediates") local flow = { type = "flow", name = data.path, style = "rcalc_rates_table_row_flow", raise_hover_events = true, game_controller_interaction = defines.game_controller_interaction and defines.game_controller_interaction.always, handler = { [defines.events.on_gui_click] = on_rates_flow_clicked, [defines.events.on_gui_hover] = on_rates_flow_hovered, [defines.events.on_gui_leave] = on_rates_flow_left, }, } if show_checkboxes then flow[#flow + 1] = { type = "checkbox", name = data.path, state = data.completed, handler = { [defines.events.on_gui_checked_state_changed] = on_completion_checkbox_checked, }, } end local button_style = "rcalc_transparent_slot" if data.path == "item/rcalc-heat-dummy" then button_style = "rcalc_transparent_slot_no_shadow" end flow[#flow + 1] = { type = "sprite-button", name = "icon", style = button_style, sprite = data.type .. "/" .. data.name, number = data.temperature, ignored_by_interaction = true, } if show_machines then flow[#flow + 1] = { type = "label", style = "rcalc_machines_label", caption = machines_caption, ignored_by_interaction = true, } flow[#flow + 1] = { type = "empty-widget", style = "flib_horizontal_pusher", ignored_by_interaction = true } end if category == "intermediates" and show_breakdown then flow[#flow + 1] = { type = "label", style = "rcalc_intermediate_breakdown_label", caption = { "", { "gui.rcalc-colored-caption", format_number(output.rate, data.is_watts, false), colors.green }, " - ", { "gui.rcalc-colored-caption", format_number(input.rate, data.is_watts, false), colors.red }, }, ignored_by_interaction = true, } end flow[#flow + 1] = { type = "label", style = "rcalc_rate_label", caption = { "gui.rcalc-colored-caption", { "", rate_caption, data.is_watts and { "si-unit-symbol-watt" } or "" }, rate_color, }, ignored_by_interaction = true, } rates_table[#rates_table + 1] = flow end flib_gui.add(parent, { type = "flow", direction = "vertical", { type = "label", style = "caption_label", caption = { "gui.rcalc-" .. category } }, rates_table, }) end local gui_rates = {} --- @param self GuiData --- @param set CalculationSet --- @return CategoryDisplayData function gui_rates.update_display_data(self, set) local timescale_data = gui_util.timescale_data[self.selected_timescale] local manual_multiplier = self.manual_multiplier local multiplier = timescale_data.multiplier or 1 local divisor, type_filter, divide_stacks, inserter_stack_size = gui_util.get_divisor(self) local dictionary = flib_dictionary.get(self.player.index, "search") or {} local show_power_input = self.player.mod_settings["rcalc-show-power-consumption"].value --[[@as boolean]] local show_pollution = self.player.mod_settings["rcalc-show-pollution"].value --[[@as boolean]] local search_query = self.search_query --- @param rate Rate --- @param is_watts boolean --- @return Rate local function scale_rate(rate, is_watts) local multiplier = is_watts and 1 or multiplier local divisor = is_watts and 1 or divisor return { machine_counts = flib_table.map(rate.machine_counts, function(count) return count * manual_multiplier end), machines = rate.machines * manual_multiplier, rate = rate.rate / (divisor or 1) * multiplier * manual_multiplier, } end --- @type table local category_display_data = { products = {}, intermediates = {}, ingredients = {}, } --- @type DisplayDataLookup local display_data_lookup = {} for path, rates in pairs(set.rates) do local is_watts = path == "item/rcalc-power-dummy" or path == "item/rcalc-heat-dummy" local output = scale_rate(rates.output, is_watts) local input = scale_rate(rates.input, is_watts) if divide_stacks and rates.type == "item" and not is_watts then local stack_size = game.item_prototypes[rates.name].stack_size output.rate = output.rate / stack_size input.rate = input.rate / stack_size end if inserter_stack_size and inserter_stack_size > 0 and rates.type == "item" and not is_watts then local stack_size = math.min(game.item_prototypes[rates.name].stack_size, inserter_stack_size) output.rate = output.rate / stack_size input.rate = input.rate / stack_size end --- @type DisplayCategory local category = "products" local sorting_rate = output.rate if output.rate > 0 and input.rate > 0 then category = "intermediates" sorting_rate = output.rate - input.rate elseif input.rate > 0 then category = "ingredients" sorting_rate = input.rate end if type_filter and (type_filter ~= rates.type or is_watts or path == "item/rcalc-pollution-dummy") then goto continue end if path == "item/rcalc-power-dummy" and not show_power_input then if output.rate > 0 then category = "products" else goto continue end end if path == "item/rcalc-pollution-dummy" and not show_pollution then goto continue end local to_search = string.lower(dictionary[path] or rates.name) if not string.find(to_search, search_query, nil, true) then goto continue end local data = { type = rates.type, name = rates.name, temperature = rates.temperature, output = output, input = input, path = path, category = category, sorting_rate = sorting_rate, completed = set.completed[path] or false, is_watts = is_watts, } local category_data = category_display_data[category] --- @type RatesDisplayData category_data[#category_data + 1] = data display_data_lookup[path] = data ::continue:: end for _, rates in pairs(category_display_data) do table.sort(rates, function(a, b) return a.sorting_rate > b.sorting_rate end) end self.display_data_lookup = display_data_lookup return category_display_data end --- @param self GuiData --- @param category_display_data CategoryDisplayData function gui_rates.update_gui(self, category_display_data) local show_checkboxes = self.player.mod_settings["rcalc-show-completion-checkboxes"].value --[[@as boolean]] local show_intermediate_breakdowns = self.player.mod_settings["rcalc-show-intermediate-breakdowns"].value --[[@as boolean]] local has_ingredients = #category_display_data.ingredients > 0 local has_intermediates = #category_display_data.intermediates > 0 local has_products = #category_display_data.products > 0 local rates_flow = self.elems.rates_flow rates_flow.clear() if has_ingredients then build_rates_table( rates_flow, "ingredients", category_display_data.ingredients, not has_intermediates and not has_products, show_checkboxes, show_intermediate_breakdowns ) if has_intermediates or has_products then rates_flow.add({ type = "line", direction = "vertical" }) end end if has_intermediates or has_products then rates_flow = rates_flow.add({ type = "flow", style = "rcalc_rates_table_vertical_flow", direction = "vertical" }) end if has_products then build_rates_table( rates_flow, "products", category_display_data.products, true, show_checkboxes, show_intermediate_breakdowns ) if has_intermediates then rates_flow.add({ type = "line", direction = "horizontal" }) end end if has_intermediates then build_rates_table( rates_flow, "intermediates", category_display_data.intermediates, true, show_checkboxes, show_intermediate_breakdowns ) end if not has_ingredients and not has_intermediates and not has_products then rates_flow.add({ type = "label", style = "rcalc_machines_label", caption = { "gui.rcalc-no-rates-to-display" } }) end end return gui_rates