--[[-- Gui - Player Bonus Adds a gui that allows players to apply various bonuses ]] local Gui = require("modules/exp_gui") local Roles = require("modules/exp_legacy/expcore/roles") local config = require("modules/exp_legacy/config/bonus") local vlayer = require("modules/exp_legacy/modules/control/vlayer") local format_number = require("util").format_number --- @class ExpGui_PlayerBonus.elements local Elements = {} --- @class ExpGui_PlayerBonus.bonus_data --- @field name string --- @field cost number --- @field scale number --- @field max_value number --- @field initial_value number --- @field is_percentage boolean --- @field is_special boolean --- @field value_step number --- @field _cost_scale number --- For perf calculate the division of scale against cost ahead of time for _, bonus_data in pairs(config.player_bonus) do bonus_data._cost_scale = bonus_data.cost / bonus_data.scale end --- Progress bar which displays how much of a bonus has been used --- @class ExpGui_PlayerBonus.elements.bonus_used: ExpElement --- @field data number --- @overload fun(parent: LuaGuiElement): LuaGuiElement Elements.bonus_used = Gui.define("player_bonus/bonus_used") :track_all_elements() :draw{ type = "progressbar", caption = "0 / 0", value = 0, style = "electric_satisfaction_statistics_progressbar", } :style{ width = 150, height = 24, font = "heading-2", color = { 1, 0, 0 }, } :element_data(0) --[[ @as any ]] --- Value is cached to save perf --- @type table do local _points_limit = {} --- Clear the cache for points limit --- @param player LuaPlayer function Elements.bonus_used._clear_points_limit_cache(player) _points_limit[player.index] = nil end --- Clear the cache for points limit --- @param player LuaPlayer --- @return number function Elements.bonus_used.calculate_points_limit(player) return _points_limit[player.index] or Elements.bonus_used._calculate_points_limit(player) end --- Calculate the bonus limit for a player --- @param player LuaPlayer --- @return number function Elements.bonus_used._calculate_points_limit(player) local role_diff = Roles.get_role_by_name(config.points.role_name).index - Roles.get_player_highest_role(player).index local points_limit = math.floor(config.points.base * (1 + config.points.increase_percentage_per_role_level * role_diff)) _points_limit[player.index] = points_limit return points_limit end end --- Refresh a bonus used slider to the current bonus cost --- @param bonus_used LuaGuiElement --- @param bonus_cost number --- @return boolean function Elements.bonus_used.refresh(bonus_used, bonus_cost) local player = Gui.get_player(bonus_used) local limit = Elements.bonus_used.calculate_points_limit(player) Elements.bonus_used.data[bonus_used] = bonus_cost bonus_used.caption = bonus_cost .. " / " .. limit bonus_used.value = bonus_cost / limit return bonus_cost <= limit end --- Refresh all bonus used sliders for a player --- @param player LuaPlayer --- @param bonus_cost number --- @return boolean function Elements.bonus_used.refresh_player(player, bonus_cost) local limit = Elements.bonus_used.calculate_points_limit(player) for _, bonus_used in Elements.bonus_used:tracked_elements(player) do Elements.bonus_used.data[bonus_used] = bonus_cost bonus_used.caption = bonus_cost .. " / " .. limit bonus_used.value = bonus_cost / limit end return bonus_cost <= limit end --- Update the element caption and value with a delta bonus cost --- @param bonus_used LuaGuiElement --- @param delta number --- @return boolean function Elements.bonus_used.update(bonus_used, delta) local player = Gui.get_player(bonus_used) local limit = Elements.bonus_used.calculate_points_limit(player) local bonus_cost = Elements.bonus_used.data[bonus_used] + delta Elements.bonus_used.data[bonus_used] = bonus_cost bonus_used.caption = bonus_cost .. " / " .. limit bonus_used.value = bonus_cost / limit return bonus_cost <= limit end --- Reset all sliders to before they were edited --- @class ExpGui_PlayerBonus.elements.reset_button: ExpElement --- @field data table --- @overload fun(parent: LuaGuiElement, bonus_table: LuaGuiElement, bonus_used: LuaGuiElement): LuaGuiElement Elements.reset_button = Gui.define("player_bonus/reset_button") :draw{ type = "sprite-button", sprite = "utility/reset", tooltip = { "exp-gui_player-bonus.tooltip-reset" }, style = "shortcut_bar_button_red", enabled = false, } :style{ size = 26, } :element_data{ bonus_table = Gui.from_argument(1), bonus_used = Gui.from_argument(2), } :on_click(function(def, player, element) --- @cast def ExpGui_PlayerBonus.elements.reset_button element.enabled = false local element_data = def.data[element] if element_data.apply_button then element_data.apply_button.enabled = false end Elements.bonus_table.reset_sliders(element_data.bonus_table) local bonus_cost = Elements.bonus_table.calculate_cost(element_data.bonus_table) Elements.bonus_used.refresh(element_data.bonus_used, bonus_cost) end) --[[ @as any ]] --- Link an apply button to this reset button so that it will be disabled after being pressed --- @param reset_button LuaGuiElement --- @param apply_button LuaGuiElement function Elements.reset_button.link_apply_button(reset_button, apply_button) Elements.reset_button.data[reset_button].apply_button = apply_button end --- Apply the bonus for a player --- @class ExpGui_PlayerBonus.elements.apply_button: ExpElement --- @field data table --- @overload fun(parent: LuaGuiElement, bonus_table: LuaGuiElement, bonus_used: LuaGuiElement): LuaGuiElement Elements.apply_button = Gui.define("player_bonus/apply_button") :draw{ type = "sprite-button", sprite = "utility/confirm_slot", tooltip = { "exp-gui_player-bonus.tooltip-apply" }, style = "shortcut_bar_button_green", enabled = false, } :style{ size = 26, } :element_data{ bonus_table = Gui.from_argument(1), bonus_used = Gui.from_argument(2), } :on_click(function(def, player, element) --- @cast def ExpGui_PlayerBonus.elements.apply_button element.enabled = false local element_data = def.data[element] if element_data.reset_button then element_data.reset_button.enabled = false end local bonus_cost = Elements.bonus_table.calculate_cost(element_data.bonus_table) if Elements.bonus_used.refresh(element_data.bonus_used, bonus_cost) then Elements.bonus_table.save_sliders(element_data.bonus_table) Elements.container.apply_player_bonus(player) end end) --[[ @as any ]] --- Link an apply button to this reset button so that it will be disabled after being pressed --- @param apply_button LuaGuiElement --- @param reset_button LuaGuiElement function Elements.apply_button.link_reset_button(apply_button, reset_button) Elements.apply_button.data[apply_button].reset_button = reset_button end --- Label used within the bonus table --- @class ExpGui_PlayerBonus.elements.bonus_table_label: ExpElement --- @overload fun(parent: LuaGuiElement, caption: LocalisedString?, tooltip: LocalisedString?, width: number?) Elements.bonus_table_label = Gui.define("player_bonus/table_label") :draw{ type = "label", caption = Gui.from_argument(1), tooltip = Gui.from_argument(2), style = "heading_2_label", } :style{ width = Gui.from_argument(3, 70), } --[[ @as any ]] --- @class ExpGui_PlayerBonus.elements.bonus_slider.elements --- @field bonus_used LuaGuiElement --- @field reset_button LuaGuiElement --- @field apply_button LuaGuiElement --- @class ExpGui_PlayerBonus.elements.bonus_slider.data: ExpGui_PlayerBonus.elements.bonus_slider.elements --- @field previous_value number --- @field label LuaGuiElement --- @field bonus_data ExpGui_PlayerBonus.bonus_data --- Slider and label pair used for selecting bonus amount --- @class ExpGui_PlayerBonus.elements.bonus_slider: ExpElement --- @field data table --- @overload fun(parent: LuaGuiElement, bonus_data: ExpGui_PlayerBonus.bonus_data, elements: ExpGui_PlayerBonus.elements.bonus_slider.elements) Elements.bonus_slider = Gui.define("player_bonus/bonus_slider") :draw(function(def, parent, bonus_data, elements) local player = Gui.get_player(parent) local value = Elements.container.get_player_bonus(player, bonus_data.name) if not value then value = bonus_data.initial_value elements.apply_button.enabled = true end local slider = parent.add{ type = "slider", value = value, maximum_value = bonus_data.max_value, value_step = bonus_data.value_step, discrete_values = true, style = "notched_slider", } slider.style.width = 180 slider.style.horizontally_stretchable = true local slider_caption = Elements.bonus_slider.calculate_slider_caption(bonus_data, value) def.data[slider] = { label = Elements.bonus_table_label(parent, slider_caption, nil, 50), previous_value = value, bonus_data = bonus_data, bonus_used = elements.bonus_used, reset_button = elements.reset_button, apply_button = elements.apply_button, } return slider end) :on_value_changed(function(def, player, element, event) --- @cast def ExpGui_PlayerBonus.elements.bonus_slider local value = element.slider_value local element_data = def.data[element] local bonus_data = element_data.bonus_data local value_change = value - element_data.previous_value element_data.previous_value = value element_data.label.caption = Elements.bonus_slider.calculate_slider_caption(bonus_data, value) element_data.apply_button.enabled = Elements.bonus_used.update(element_data.bonus_used, value_change * bonus_data._cost_scale) element_data.reset_button.enabled = true end) --[[ @as any ]] --- Get the caption of the slider label --- @param bonus_data ExpGui_PlayerBonus.bonus_data --- @param value number --- @return LocalisedString function Elements.bonus_slider.calculate_slider_caption(bonus_data, value) return bonus_data.is_percentage and format_number(value * 100, false) .. " %" or format_number(value, false) end --- Calculate the cost of a slider --- @param slider LuaGuiElement --- @return number function Elements.bonus_slider.calculate_cost(slider) local bonus_data = Elements.bonus_slider.data[slider].bonus_data return slider.slider_value * bonus_data._cost_scale end --- Reset a slider to its original value --- @param slider LuaGuiElement function Elements.bonus_slider.reset_value(slider) local player = Gui.get_player(slider) local element_data = Elements.bonus_slider.data[slider] local bonus_data = element_data.bonus_data local value = Elements.container.get_player_bonus(player, bonus_data.name) or bonus_data.initial_value slider.slider_value = value element_data.label.caption = Elements.bonus_slider.calculate_slider_caption(bonus_data, value) element_data.previous_value = value end --- Save a slider at its current value --- @param slider LuaGuiElement function Elements.bonus_slider.save_value(slider) local player = Gui.get_player(slider) local bonus_data = Elements.bonus_slider.data[slider].bonus_data Elements.container.set_player_bonus(player, bonus_data.name, slider.slider_value) end --- A table containing all of the bonus sliders and their label --- @class ExpGui_PlayerBonus.elements.bonus_table: ExpElement --- @field data table --- @overload fun(parent: LuaGuiElement): LuaGuiElement Elements.bonus_table = Gui.define("player_bonus/bonus_table") :draw(function(_, parent) return Gui.elements.scroll_table(parent, 300, 3) end) :element_data{} --[[ @as any ]] --- Adds a row to the milestone table --- @param bonus_table LuaGuiElement --- @param elements ExpGui_PlayerBonus.elements.bonus_slider.elements --- @param bonus_data ExpGui_PlayerBonus.bonus_data function Elements.bonus_table.add_row(bonus_table, bonus_data, elements) local rows = Elements.bonus_table.data[bonus_table] Elements.bonus_table_label(bonus_table, { "exp-gui_player-bonus.caption-" .. bonus_data.name }, { "exp-gui_player-bonus.tooltip-" .. bonus_data.name }) rows[#rows + 1] = Elements.bonus_slider(bonus_table, bonus_data, elements) end --- Calculate the total cost of a table --- @param bonus_table LuaGuiElement --- @return number function Elements.bonus_table.calculate_cost(bonus_table) local cost = 0 for _, slider in pairs(Elements.bonus_table.data[bonus_table]) do cost = cost + Elements.bonus_slider.calculate_cost(slider) end return cost end --- Reset all sliders in the table to their original positions --- @param bonus_table LuaGuiElement function Elements.bonus_table.reset_sliders(bonus_table) for _, slider in pairs(Elements.bonus_table.data[bonus_table]) do Elements.bonus_slider.reset_value(slider) end end --- Save all sliders at their current position --- @param bonus_table LuaGuiElement function Elements.bonus_table.save_sliders(bonus_table) for _, slider in pairs(Elements.bonus_table.data[bonus_table]) do Elements.bonus_slider.save_value(slider) end end --- Container added to the left gui flow --- @class ExpGui_PlayerBonus.elements.container: ExpElement --- @field data table Elements.container = Gui.define("player_bonus/container") :draw(function(def, parent) local container = Gui.elements.container(parent) local header = Gui.elements.header(container, { caption = { "exp-gui_player-bonus.caption-main" } }) local player = Gui.get_player(parent) def.data[player] = def.data[player] or {} -- Used within bonus_table.add_row local elements = {} --- @cast elements ExpGui_PlayerBonus.elements.bonus_slider.elements local bonus_table = Elements.bonus_table(container) elements.bonus_used = Elements.bonus_used(header) elements.reset_button = Elements.reset_button(header, bonus_table, elements.bonus_used) elements.apply_button = Elements.apply_button(header, bonus_table, elements.bonus_used) Elements.reset_button.link_apply_button(elements.reset_button, elements.apply_button) Elements.apply_button.link_reset_button(elements.apply_button, elements.reset_button) for _, bonus_data in pairs(config.player_bonus) do --- @cast bonus_data ExpGui_PlayerBonus.bonus_data Elements.bonus_table.add_row(bonus_table, bonus_data, elements) end local bonus_cost = Elements.bonus_table.calculate_cost(bonus_table) Elements.bonus_used.refresh(elements.bonus_used, bonus_cost) return Gui.elements.container.get_root_element(container) end) --[[ @as any ]] --- Set the bonus value for a player --- @param player LuaPlayer --- @param name string --- @param value number function Elements.container.set_player_bonus(player, name, value) Elements.container.data[player][name] = value end --- Get the bonus value for a player --- @param player LuaPlayer --- @param name string --- @return number function Elements.container.get_player_bonus(player, name) return Elements.container.data[player][name] end --- Clear all bonus values for a player --- @param player LuaPlayer function Elements.container.clear_player_bonus(player) Elements.container.data[player] = {} if not player.character then return end for _, bonus_data in pairs(config.player_bonus) do if not bonus_data.is_special then player[bonus_data.name] = 0 if bonus_data.combined_bonus then for _, name in ipairs(bonus_data.combined_bonus) do player[name] = 0 end end end end end --- Apply all bonus values for a player --- @param player LuaPlayer function Elements.container.apply_player_bonus(player) if not player.character then return end local player_data = Elements.container.data[player] for _, bonus_data in pairs(config.player_bonus) do if not bonus_data.is_special then local value = player_data[bonus_data.name] or 0 player[bonus_data.name] = value if bonus_data.combined_bonus then for _, name in ipairs(bonus_data.combined_bonus) do player[name] = value end end end end end --- Calculate the current cost for a player --- @param player LuaPlayer --- @return number function Elements.container.calculate_cost(player) local cost = 0 local player_data = Elements.container.data[player] for _, bonus_data in pairs(config.player_bonus) do cost = cost + (player_data[bonus_data.name] or 0) * bonus_data._cost_scale end return cost 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_bonus", left_element = Elements.container, sprite = "item/exoskeleton-equipment", tooltip = { "exp-gui_player-bonus.tooltip-main" }, visible = function(player, element) return Roles.player_allowed(player, "gui/bonus") end } --- Recalculate and apply the bonus for a player local function recalculate_bonus(event) local player = Gui.get_player(event) if event.name == Roles.events.on_role_assigned or event.name == Roles.events.on_role_unassigned then -- If the player's roles changed then we will need to recalculate their limit Elements.bonus_used._clear_points_limit_cache(player) local bonus_cost = Elements.container.calculate_cost(player) local within_limit = Elements.bonus_used.refresh_player(player, bonus_cost) if not within_limit or not Roles.player_allowed(player, "gui/bonus") then Elements.container.clear_player_bonus(player) return end end Elements.container.apply_player_bonus(player) end --- Apply periodic bonus to a player --- @param player LuaPlayer local function apply_personal_battery_recharge(player) local available_energy = vlayer.get_statistics()["energy_storage"] if available_energy <= 0 then return -- No power to give end local armor = player.get_inventory(defines.inventory.character_armor) if not armor or not armor[1] or not armor[1].valid_for_read then return -- No armor end local grid = armor[1].grid if not grid or grid.available_in_batteries >= grid.battery_capacity then return -- No grid or already full end local recharge_amount = Elements.container.get_player_bonus(player, "personal_battery_recharge") * 100000 * config.periodic_bonus_rate / 6 for _, equipment in pairs(grid.equipment) do if equipment.energy < equipment.max_energy then local energy_to_give = math.min(math.floor(equipment.max_energy - equipment.energy), available_energy, recharge_amount) equipment.energy = equipment.energy + energy_to_give recharge_amount = recharge_amount - energy_to_give available_energy = vlayer.energy_changed(-energy_to_give) end end end --- Apply the periodic bonus to all players local function apply_periodic_bonus_online() for _, player in pairs(game.connected_players) do if player.character and Roles.player_allowed(player, "gui/bonus") then apply_personal_battery_recharge(player) end end end local e = defines.events return { elements = Elements, events = { [e.on_player_respawned] = recalculate_bonus, [Roles.events.on_role_assigned] = recalculate_bonus, [Roles.events.on_role_unassigned] = recalculate_bonus, }, on_nth_tick = { [config.periodic_bonus_rate] = apply_periodic_bonus_online, } }