require 'utils.table' local Global = require 'utils.global' local Gui = require 'utils.gui' local Event = require 'utils.event' local PlayerStats = require 'features.player_stats' local Game = require 'utils.game' local math = require 'utils.math' local format = string.format local size = table.size local insert = table.insert local pairs = pairs local tonumber = tonumber local clamp = math.clamp local floor = math.floor local ceil = math.ceil local raise_event = script.raise_event local market_frame_name = Gui.uid_name() local market_frame_close_button_name = Gui.uid_name() local item_button_name = Gui.uid_name() local count_slider_name = Gui.uid_name() local count_text_name = Gui.uid_name() local color_red = {r = 255, b = 0, g = 0} local color_dark_grey = {r = 169, g = 169, b = 169} local Retailer = {} Retailer.events = { --- Triggered when a purchase is made -- Event { -- item = item, -- count = count, -- player = player, -- group_name = group_name, -- } on_market_purchase = script.generate_event_name(), } ---Global storage ---Markets are indexed by the position "x,y" and contains the group it belongs to ---Items are indexed by the group name and is a list indexed by the item name and contains the prices per item local memory = { markets = {}, items = {}, group_label = {}, } Global.register({ memory = memory, }, function (tbl) memory = tbl.memory end) ---Sets the name of the market group, provides a user friendly label in the GUI. ---@param group_name string ---@param label string function Retailer.set_market_group_label(group_name, label) memory.group_label[group_name] = label end ---Gets the name of the market group. ---@param group_name string function Retailer.get_market_group_label(group_name) return memory.group_label[group_name] or 'Market' end ---Returns all item for the group_name retailer. ---@param group_name string function Retailer.get_items(group_name) return memory.items[group_name] or {} end ---Removes an item from the markets for the group_name retailer. ---@param group_name string ---@param item_name string function Retailer.remove_item(group_name, item_name) if not memory.items[group_name] then return end memory.items[group_name][item_name] = nil end local function redraw_market_items(data) local grid = data.grid Gui.clear(grid) local count = data.count local market_items = data.market_items local player_coins = data.player_coins if size(market_items) == 0 then grid.add({type = 'label', caption = 'No items available at this time'}) return end for i, item in pairs(market_items) do local price = item.price local tooltip = {'', item.name_label, format('\nprice: %d', price)} local description = item.description local total_price = ceil(price * count) local disabled = item.disabled == true local message if total_price == 1 then message = '1 coin' else message = total_price .. ' coins' end local missing_coins = total_price - player_coins local is_missing_coins = missing_coins > 0 if description then insert(tooltip, '\n' .. item.description) end if disabled then insert(tooltip, '\n\n' .. (item.disabled_reason or 'Not available')) elseif is_missing_coins then insert(tooltip, '\n\n' .. format('Missing %d coins to buy %d', missing_coins, count)) end local button = grid.add({type = 'flow'}).add({ type = 'sprite-button', name = item_button_name, sprite = item.sprite, number = count, tooltip = tooltip, }) button.style = 'slot_button' Gui.set_data(button, {index = i, data = data}) local label = grid.add({type = 'label', caption = message}) local label_style = label.style label_style.width = 93 label_style.height = 32 label_style.font = 'default-bold' label_style.vertical_align = 'center' if disabled then label_style.font_color = color_dark_grey button.enabled = false elseif is_missing_coins then label_style.font_color = color_red button.enabled = false end end end local function do_coin_label(coin_count, label) if coin_count == 1 then label.caption = '1 coin available' else label.caption = coin_count .. ' coins available' end label.style.font = 'default-bold' end local function draw_market_frame(player, group_name) local frame = player.gui.center.add({ type = 'frame', name = market_frame_name, caption = Retailer.get_market_group_label(group_name), direction = 'vertical', }) local scroll_pane = frame.add({type = 'scroll-pane'}) local scroll_style = scroll_pane.style scroll_style.maximal_height = 600 local grid = scroll_pane.add({type = 'table', column_count = 10}) local market_items = Retailer.get_items(group_name) local player_coins = player.get_item_count('coin') local data = { grid = grid, count = 1, market_items = market_items, player_coins = player_coins, market_group = group_name, } local coin_label = frame.add({type = 'label'}) do_coin_label(player_coins, coin_label) data.coin_label = coin_label redraw_market_items(data) local bottom_grid = frame.add({type = 'table', column_count = 2}) bottom_grid.add({type = 'label', caption = 'Quantity: '}).style.font = 'default-bold' local count_text = bottom_grid.add({ type = 'text-box', name = count_text_name, text = '1', }) local count_slider = frame.add({ type = 'slider', name = count_slider_name, minimum_value = 1, maximum_value = 7, value = 1, }) frame.add({name = market_frame_close_button_name, type = 'button', caption = 'Close'}) count_slider.style.width = 115 count_text.style.width = 45 data.slider = count_slider data.text = count_text Gui.set_data(count_slider, data) Gui.set_data(count_text, data) return frame end Event.add(defines.events.on_gui_opened, function (event) if not event.gui_type == defines.gui_type.entity then return end local player = Game.get_player_by_index(event.player_index) if not player or not player.valid then return end local entity = event.entity if not entity or not entity.valid then return end local position = entity.position local group_name = memory.markets[position.x .. ',' .. position.y] if not group_name then return end local frame = draw_market_frame(player, group_name) player.opened = frame end) Gui.on_custom_close(market_frame_name, function (event) local element = event.element Gui.destroy(element) end) local function close_market_gui(player) local element = player.gui.center if element and element.valid then element = element[market_frame_name] if element and element.valid then Gui.destroy(element) end end end Gui.on_click(market_frame_close_button_name, function (event) close_market_gui(event.player) end) Event.add(defines.events.on_player_died, function (event) local player = Game.get_player_by_index(event.player_index or 0) if not player or not player.valid then return end close_market_gui(player) end) Gui.on_value_changed(count_slider_name, function (event) local element = event.element local data = Gui.get_data(element) local value = floor(element.slider_value) local count if value % 2 == 0 then count = 10 ^ (value * 0.5) * 0.5 else count = 10 ^ ((value - 1) * 0.5) end data.count = count data.text.text = count redraw_market_items(data) end) Gui.on_text_changed(count_text_name, function (event) local element = event.element local data = Gui.get_data(element) local count = tonumber(element.text) if count then count = floor(count) count = clamp(count, 1, 1000) data.count = count data.text.text = count else data.count = 1 end redraw_market_items(data) end) Gui.on_click(item_button_name, function (event) local player = event.player local element = event.element local button_data = Gui.get_data(element) local data = button_data.data local item = data.market_items[button_data.index] if item.disabled then player.print({'', item.name_label, ' is disabled. ', item.disabled_reason or ''}) return end local name = item.name local price = item.price local count = data.count local cost = ceil(price * count) local coin_count = player.get_item_count('coin') if cost > coin_count then player.print('Insufficient coins') return end if item.type == 'item' then local inserted = player.insert({name = name, count = count}) if inserted < count then player.print('Insufficient inventory space') if inserted > 0 then player.remove_item({name = name, count = inserted}) end return end end player.remove_item({name = 'coin', count = cost}) local player_coins = data.player_coins - cost data.player_coins = player_coins do_coin_label(player_coins, data.coin_label) redraw_market_items(data) PlayerStats.change_coin_spent(player.index, cost) raise_event(Retailer.events.on_market_purchase, { item = item, count = count, player = player, }) end) ---Add a market to the group_name retailer. ---@param group_name string ---@param market_entity LuaEntity function Retailer.add_market(group_name, market_entity) local position = market_entity.position memory.markets[position.x .. ',' .. position.y] = group_name end ---Sets an item for all the group_name markets. ---@param group_name string ---@param prototype table with item name and price function Retailer.set_item(group_name, prototype) if not memory.items[group_name] then memory.items[group_name] = {} end local item_name = prototype.name local name_label = prototype.name_label if not name_label then local item_prototype = game.item_prototypes[item_name] name_label = item_prototype and item_prototype.localised_name end prototype.name_label = name_label or item_name prototype.sprite = prototype.sprite or 'item/' .. item_name prototype.type = prototype.type or 'item' memory.items[group_name][item_name] = prototype end ---Enables a market item by group name and item name if it's registered. ---@param group_name string ---@param item_name string function Retailer.enable_item(group_name, item_name) if not memory.items[group_name] then return end local prototype = memory.items[group_name][item_name] if not prototype then return end prototype.disabled = false prototype.disabled_reason = false end ---Disables a market item by group name and item name if it's registered. ---@param group_name string ---@param item_name string ---@param disabled_reason string function Retailer.disable_item(group_name, item_name, disabled_reason) if not memory.items[group_name] then return end local prototype = memory.items[group_name][item_name] if not prototype then return end prototype.disabled = true prototype.disabled_reason = disabled_reason end return Retailer