From 68b2dda190243f47f90d9c900ffb9805bc67c70f Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Sat, 5 Sep 2026 19:50:27 +0000 Subject: [PATCH] Move the warp list into exp_scenario The warp system was split across a legacy control module and a legacy gui, both built on expcore.datastore. They are now one exp_scenario gui, exp_scenario/module/gui/warp_list.lua, in the shape of the task list: per force warps and per player cooldown and proximity state live on the container's element data, rows are element defines which carry their warp in element data, and refreshes compute the player's status once and apply it to every row. Warps are referenced as objects rather than string ids, the last editor is a LuaPlayer, and the cooldown counts ticks. The locale moves to [exp-gui_warp-list] in en, zh-CN and zh-TW with keys named by region and use, and the legacy [warp-list] sections are removed along with the two legacy modules and the file loader entry. The config stays in exp_legacy/config/gui/warps.lua for now like the other gui configs. --- exp_legacy/module/config/_file_loader.lua | 1 - exp_legacy/module/locale/en.cfg | 31 - exp_legacy/module/locale/zh-CN.cfg | 31 - exp_legacy/module/locale/zh-TW.cfg | 31 - exp_legacy/module/modules/control/warps.lua | 488 -------- exp_legacy/module/modules/gui/warp-list.lua | 909 -------------- exp_scenario/module/control.lua | 1 + exp_scenario/module/gui/warp_list.lua | 1218 +++++++++++++++++++ exp_scenario/module/locale/en.cfg | 31 + exp_scenario/module/locale/zh-CN.cfg | 31 + exp_scenario/module/locale/zh-TW.cfg | 31 + 11 files changed, 1312 insertions(+), 1491 deletions(-) delete mode 100644 exp_legacy/module/modules/control/warps.lua delete mode 100644 exp_legacy/module/modules/gui/warp-list.lua create mode 100644 exp_scenario/module/gui/warp_list.lua diff --git a/exp_legacy/module/config/_file_loader.lua b/exp_legacy/module/config/_file_loader.lua index 4edd462e..31fe77ed 100644 --- a/exp_legacy/module/config/_file_loader.lua +++ b/exp_legacy/module/config/_file_loader.lua @@ -20,7 +20,6 @@ return { --"modules.data.toolbar", --- GUI - "modules.gui.warp-list", "modules.gui.vlayer", "modules.graftorio.require", -- graftorio diff --git a/exp_legacy/module/locale/en.cfg b/exp_legacy/module/locale/en.cfg index 708296e0..af6bca63 100644 --- a/exp_legacy/module/locale/en.cfg +++ b/exp_legacy/module/locale/en.cfg @@ -158,37 +158,6 @@ ban-player=Ban player afk-time=__1__% of total map time\nLast moved __2__ ago open-map=__1__ __2__\n__3__\nClick to open map -[warp-list] -main-caption=Warp List [img=info] -main-tooltip=Warp List -sub-tooltip=Warps can only be used every __1__ seconds and when within __2__ tiles\n__3__\n__4__\n__5__\n__6__\n__7__\n__8__ -sub-tooltip-current= - __1__ This is your current warp point; -sub-tooltip-connected= - __1__ You can travel to this warp point; -sub-tooltip-different= - __1__ This warp is on a different network; -sub-tooltip-cooldown= - __1__ You are currently on cooldown; -sub-tooltip-not_available= - __1__ You are not in range of a warp point; -sub-tooltip-bypass= - __1__ Your role allows you to travel here; -too-close=Cannot create warp; too close to existing warp point: __1__ -too-close-to-water=Cannot create warp; too close to water, please move __1__ tiles away from the water body or landfill it -too-close-to-entities=Cannot create warp; too close to other entities, please move __1__ tiles away from the entities or remove them -last-edit=Last edited by __1__ at __2__\nClick to view on map -add-tooltip=Add new warp -confirm-tooltip=Save changes -cancel-tooltip=Discard changes -edit-tooltip=Currently being edited by: __1__ -edit-tooltip-none=Currently being edited by: Nobody -remove-tooltip=Remove warp -timer-tooltip=Please wait __1__ seconds before you can warp -timer-tooltip-zero=After each warp you will need to wait __1__ seconds before you can warp again -goto-tooltip=Go to x __1__ y __2__ -goto-bypass=Go to x __1__ y __2__, bypass mode -goto-bypass-different-network=Go to x __1__ y __2__, bypass mode different network -goto-same-warp=You are already on this warp -goto-different-network=This warp is on a different network, use power poles to connect it -goto-cooldown=You are on cooldown, wait for your cooldown to recharge -goto-disabled=You are not on a warp point, walk to a warp point -goto-edit=Edit warp icon - [bonus] description=Get / Set the amount of bonus you receive. arg-amount=Amount to set your bonus to, 0 will disable bonus. diff --git a/exp_legacy/module/locale/zh-CN.cfg b/exp_legacy/module/locale/zh-CN.cfg index 02a1efba..28178f71 100644 --- a/exp_legacy/module/locale/zh-CN.cfg +++ b/exp_legacy/module/locale/zh-CN.cfg @@ -158,37 +158,6 @@ ban-player=封禁用戶 afk-time=__1__% 地圖時間\n上次移動: __2__ open-map=__1__ __2__\n__3__\n按下打開地圖 -[warp-list] -main-caption=傳送陣清單 [img=info] -main-tooltip=傳送陣清單 -sub-tooltip=傳送陣在 __1__ 秒內只能使用一次。 亦需在 __2__ 格之內使用\n__3__\n__4__\n__5__\n__6__\n__7__\n__8__ -sub-tooltip-current= - __1__ 你目前的傳送陣; -sub-tooltip-connected= - __1__ 你可以到本傳送陣; -sub-tooltip-different= - __1__ 該傳送陣未連接; -sub-tooltip-cooldown= - __1__ 傳送冷卻中; -sub-tooltip-not_available= - __1__ 你不在傳送陣範圍; -sub-tooltip-bypass= - __1__ 你可以傳送到這裏; -too-close=無法新建傳送陣; 太接近 __1__ 了 -too-close-to-water=無法新建傳送陣; 太接近水源了, 請移動 __1__ 格又或者填平該地方 -too-close-to-entities=無法新建傳送陣; 太接近其他東西, 請移動 __1__ 格又或者拆除該東西 -last-edit=最後由 __1__ 在 __2__ 更改\n按這在地圖上看 -add-tooltip=新建傳送陣 -confirm-tooltip=儲存更改 -cancel-tooltip=放棄更改 -edit-tooltip=現被 __1__ 修改中 -edit-tooltip-none=現沒有被人修改 -remove-tooltip=拆除傳送陣 -timer-tooltip=請等 __1__ 秒才再次傳送 -timer-tooltip-zero=你要等 __1__ 秒才可以再次傳送 -goto-tooltip=到 X __1__ Y __2__ -goto-bypass=到 X __1__ Y __2__, 繞行模式 -goto-bypass-different-network=到 X __1__ Y __2__, 繞行模式 (不同網絡) -goto-same-warp=你已在該傳送陣 -goto-different-network=該傳送陣未連接, 你可使用電纜連接 -goto-cooldown=傳送冷卻中, 請等候冷卻 -goto-disabled=你不在傳送陣, 請移至任意傳送陣 -goto-edit=修改傳送陣圖案 - [bonus] description=取得 / 設定 Bonus 量。 arg-amount=Bonus 數量, 0 來停用。 diff --git a/exp_legacy/module/locale/zh-TW.cfg b/exp_legacy/module/locale/zh-TW.cfg index 02a1efba..28178f71 100644 --- a/exp_legacy/module/locale/zh-TW.cfg +++ b/exp_legacy/module/locale/zh-TW.cfg @@ -158,37 +158,6 @@ ban-player=封禁用戶 afk-time=__1__% 地圖時間\n上次移動: __2__ open-map=__1__ __2__\n__3__\n按下打開地圖 -[warp-list] -main-caption=傳送陣清單 [img=info] -main-tooltip=傳送陣清單 -sub-tooltip=傳送陣在 __1__ 秒內只能使用一次。 亦需在 __2__ 格之內使用\n__3__\n__4__\n__5__\n__6__\n__7__\n__8__ -sub-tooltip-current= - __1__ 你目前的傳送陣; -sub-tooltip-connected= - __1__ 你可以到本傳送陣; -sub-tooltip-different= - __1__ 該傳送陣未連接; -sub-tooltip-cooldown= - __1__ 傳送冷卻中; -sub-tooltip-not_available= - __1__ 你不在傳送陣範圍; -sub-tooltip-bypass= - __1__ 你可以傳送到這裏; -too-close=無法新建傳送陣; 太接近 __1__ 了 -too-close-to-water=無法新建傳送陣; 太接近水源了, 請移動 __1__ 格又或者填平該地方 -too-close-to-entities=無法新建傳送陣; 太接近其他東西, 請移動 __1__ 格又或者拆除該東西 -last-edit=最後由 __1__ 在 __2__ 更改\n按這在地圖上看 -add-tooltip=新建傳送陣 -confirm-tooltip=儲存更改 -cancel-tooltip=放棄更改 -edit-tooltip=現被 __1__ 修改中 -edit-tooltip-none=現沒有被人修改 -remove-tooltip=拆除傳送陣 -timer-tooltip=請等 __1__ 秒才再次傳送 -timer-tooltip-zero=你要等 __1__ 秒才可以再次傳送 -goto-tooltip=到 X __1__ Y __2__ -goto-bypass=到 X __1__ Y __2__, 繞行模式 -goto-bypass-different-network=到 X __1__ Y __2__, 繞行模式 (不同網絡) -goto-same-warp=你已在該傳送陣 -goto-different-network=該傳送陣未連接, 你可使用電纜連接 -goto-cooldown=傳送冷卻中, 請等候冷卻 -goto-disabled=你不在傳送陣, 請移至任意傳送陣 -goto-edit=修改傳送陣圖案 - [bonus] description=取得 / 設定 Bonus 量。 arg-amount=Bonus 數量, 0 來停用。 diff --git a/exp_legacy/module/modules/control/warps.lua b/exp_legacy/module/modules/control/warps.lua deleted file mode 100644 index fdca5b62..00000000 --- a/exp_legacy/module/modules/control/warps.lua +++ /dev/null @@ -1,488 +0,0 @@ ---[[-- Control Module - Warps -- Stores warps for each force. -@control Warps -@alias Warps - -@usage-- Making a new spawn warp -local player = game.player -local force = player.force -local spawn_id = Warps.add_warp(force.name, player.physical_surface, player.physical_position, player.name, 'Spawn') - -Warps.set_spawn_warp(spawn_id, force) -Warps.make_warp_tag(spawn_id) - -@usage-- Making a new warp with a warp area -local player = game.player -local force = player.force -local warp_id = Warps.add_warp(force.name, player.physical_surface, player.physical_position, player.name) - -Warps.make_warp_area(warp_id) -Warps.make_warp_tag(warp_id) - -]] - -local Datastore = require("modules.exp_legacy.expcore.datastore") --- @dep expcore.datastore -local Storage = require("modules/exp_util/storage") -local config = require("modules.exp_legacy.config.gui.warps") --- @dep config.warps - ---- Stores all data for the warp system -local WrapData = Datastore.connect("WrapData") -WrapData:set_serializer(function(raw_key) return raw_key.warp_id end) - -local Warps = {} - ---- @class ExpScenario_Warps.force_warps: { spawn: string, [number]: string } - --- Storage lookup table for force name to task ids -local force_warps = { _uid = 1 } ---- @cast force_warps table -Storage.register(force_warps, function(tbl) - force_warps = tbl -end) - --- Create an array of entity names that will be added to the remove filter -local remove_warp_area_entity_names = {} -for _, entity in pairs(config.entities) do - table.insert(remove_warp_area_entity_names, entity[1]) -end - --- When a warp is updated change its chat tag and restore the warp order -WrapData:on_update(function(warp_id, warp, old_warp) - if warp then - warp.updates = warp.updates + 1 - -- Update the map chart tag if there is one - if warp.tag then - Warps.make_warp_tag(warp_id) - end - - -- Check that the name of the warp has been changed - if not old_warp or warp.name == old_warp.name then return end - - -- Get the names of all the warp points for this force - local force_name = warp.force_name - local warp_ids = force_warps[force_name] - local spawn_id = warp_ids.spawn - - local warp_names = {} --- @type table - for _, next_warp_id in pairs(warp_ids) do - local next_warp = WrapData:get(next_warp_id) - if next_warp_id ~= spawn_id then - warp_names[next_warp.name .. next_warp_id] = next_warp_id - end - end - - -- Sort the warp names in alphabetical order - local new_warp_ids = table.get_values(table.key_sort(warp_names)) - --- @cast new_warp_ids ExpScenario_Warps.force_warps - table.insert(new_warp_ids, 1, spawn_id) - new_warp_ids.spawn = spawn_id - force_warps[force_name] = new_warp_ids - end -end) - ---- Map Integration. --- functions used to create and alter warps with in the map --- @section mapIntegration - ---[[-- Add or update the chat tag for this warp -@tparam string warp_id the uid of the warp you want the chart tag for -@treturn boolean true if a new tag was made, false if it was updated - -@usage-- Adding a chart tag for a new warp -local tag_added = Warps.make_warp_tag(warp_id) - -]] -function Warps.make_warp_tag(warp_id) - local warp = WrapData:get(warp_id) - local name = warp.name - local icon = warp.icon - - -- Edit the existing tag if it is present - local tag = warp.tag - - if tag and tag.valid then - tag.text = "Warp: " .. name - tag.icon = icon - return false - end - - -- Make a new tag if one did not exist - local force = game.forces[warp.force_name] - local surface = warp.surface - local position = warp.position - - tag = force.add_chart_tag(surface, { - position = { position.x + 0.5, position.y + 0.5 }, - text = "Warp: " .. name, - icon = icon, - }) - - -- Add the tag to this warp, store.update not needed as we dont want it to trigger - warp.tag = tag - return true -end - ---[[-- Remove the chart tag for this warp -@tparam string warp_id the uid for the warp that you want to remove the chart tag from -@treturn boolean true if the tag was valid and was removed, false if the tag was invalid - -@usage-- Removing the chart tag from a warp -local removed = Warps.remove_warp_tag(warp_id) - -]] -function Warps.remove_warp_tag(warp_id) - local warp = WrapData:get(warp_id) - - -- Check there is a tag to remove - local tag = warp.tag - if not tag or not tag.valid then - warp.tag = nil - return false - end - - -- Remove the warp chart tag if it is valid - tag.destroy() - warp.tag = nil - - return true -end - ---[[-- Add a warp area for the warp, purely cosmetic -@tparam string warp_id the uid of the warp you want the area for - -@usage-- Adding a warp area for a warp -Warps.make_warp_area(warp_id) - -]] -function Warps.make_warp_area(warp_id) - local warp = WrapData:get(warp_id) - local surface = warp.surface - local position = warp.position - local posx = position.x - local posy = position.y - - -- Get the tile that is being replaced, store.update not needed as we don't want it to trigger - local old_tile = surface.get_tile(position).name - warp.old_tile = old_tile - - -- Add a tile pattern on top of the base - local tiles = {} - for _, tile in pairs(config.tiles) do - table.insert(tiles, { name = tile[1], position = { tile[2] + posx, tile[3] + posy } }) - end - - surface.set_tiles(tiles) - - -- Add entities to the warp structure - for _, entity in pairs(config.entities) do - entity = surface.create_entity{ - name = entity[1], - position = { entity[2] + posx, entity[3] + posy }, - force = "neutral", - } - entity.destructible = false - entity.health = 0 - entity.minable_flag = false - entity.rotatable = false - - -- Save reference of the last power pole - if entity.type == "electric-pole" then - warp.electric_pole = entity - end - end -end - ---[[-- Remove the warp area for a warp -@tparam string warp_id the uid of the warp that you want to remove the area for - -@usage-- Remove the warp area for a warp -Warps.remove_warp_area(warp_id) - -]] -function Warps.remove_warp_area(warp_id) - local warp = WrapData:get(warp_id) - local position = warp.position - local surface = warp.surface - local radius = config.standard_proximity_radius - - -- Check that a warp area was created previously - local old_tile = warp.old_tile - if not old_tile then return end - - -- Restore the original tiles before the creation of the warp - local tiles = {} - - for _, tile in pairs(config.tiles) do - table.insert(tiles, { name = old_tile, position = { tile[2] + position.x, tile[3] + position.y } }) - end - - surface.set_tiles(tiles) - - local area = { - { position.x - radius, position.y - radius }, - { position.x + radius, position.y + radius }, - } - - -- Remove warp structure entities - local entities = surface.find_entities_filtered{ force = "neutral", area = area, name = remove_warp_area_entity_names } - for _, entity in pairs(entities) do - -- Destroy them, this will leave corpses of the entities that it destroyed. - if entity and entity.valid and entity.destructible == false then - entity.destructible = true - entity.die(entity.force) - end - end - - -- Rechart map area, useful if warp is not covered by a radar - game.forces[warp.force_name].chart(surface, area) -end - ---[[-- Set a warp to be the spawn point for a force, force must own this warp -@tparam string warp_id the uid of the warp that you want to be the spawn for the force -@tparam LuaForce force the force that you want to set the spawn for - -@usage-- Set your forces spawn to a warp -Warps.set_spawn_warp(warp_id, game.player.force) - -]] -function Warps.set_spawn_warp(warp_id, force) - -- Check the force owns this warp - local warp = WrapData:get(warp_id) - if warp.force_name ~= force.name then - return - end - -- Set this warp as the spawn - local warp_ids = force_warps[warp.force_name] - if not warp_ids then - warp_ids = {} - force_warps[warp.force_name] = warp_ids - end - warp_ids.spawn = warp_id - -- Set the forces spawn to this warp - force.set_spawn_position(warp.position, warp.surface) -end - ---[[-- Teleport a player to a warp point -@tparam string warp_id the uid of the warp to send the player to -@tparam LuaPlayer player the player to teleport to the warp - -@usage-- Teleport yourself to a warp point -Warps.teleport_player(warp_id, game.player) - -]] -function Warps.teleport_player(warp_id, player) - local warp = WrapData:get(warp_id) - local surface = warp.surface - local position = { - x = warp.position.x + 0.5, - y = warp.position.y + 0.5, - } - - if player.vehicle then - -- Teleport the entity - local entity = player.vehicle - local goto_position = surface.find_non_colliding_position(entity.name, position, 32, 1) - -- Surface teleport can only be done for players and cars at the moment. (with surface as an peramitor it gives this error) - if entity.type == "car" then - entity.teleport(goto_position, surface) - elseif surface.index == entity.surface.index then - -- Try teleport the entity - if not entity.teleport(goto_position) then - player.driving = false - -- Need to calculate new goto_position because entities have different collision boxes - goto_position = surface.find_non_colliding_position("character", position, 32, 1) - player.teleport(goto_position, surface) - end - end - else - -- Teleport the player - local goto_position = surface.find_non_colliding_position("character", position, 32, 1) - if player.driving then player.driving = false end - player.teleport(goto_position, surface) - end -end - ---- Setters. --- functions used to created and alter warps --- @section setters - ---[[-- Add a new warp for a force, the warp must have a surface and a position -@tparam string force_name the name of the force to add the warp for -@tparam LuaSurface surface the surface that the warp will be on -@tparam Concepts.Position position the position that the warp will be on -@tparam[opt] string player_name the name of the player that is adding the warp -@tparam[opt] string warp_name the name of the warp that is being added, if omited default is used -@treturn string the uid of the warp which was created - -@usage-- Adding a new warp for your force at your position -local player = game.player -local warp_id = Warps.add_warp(player.force.name, player.physical_surface, player.physical_position, player.name) - -]] -function Warps.add_warp(force_name, surface, position, player_name, warp_name) - -- Get new warp id - local warp_id = tostring(force_warps._uid) - force_warps._uid = force_warps._uid + 1 - warp_name = warp_name or "New warp" - - -- Get the existing warps for this force - local warp_ids = force_warps[force_name] - if not warp_ids then - warp_ids = {} - force_warps[force_name] = warp_ids - end - - -- Insert the warp id into the force warps - table.insert(warp_ids, warp_id) - - -- Create the editing table - local editing = {} - if player_name then - editing[player_name] = true - end - - -- Add the new warp to the store - WrapData:set(warp_id, { - warp_id = warp_id, - force_name = force_name, - name = warp_name, - icon = { type = config.default_icon.type, name = config.default_icon.name }, - surface = surface, - position = { - x = math.floor(position.x), - y = math.floor(position.y), - }, - last_edit_name = player_name or "", - last_edit_time = game.tick, - currently_editing = editing, - updates = 0, - }) - - return warp_id -end - ---[[-- Removes a warp and any data linked to it -@tparam string warp_id the uid of the warp that you want to remove - -@usage-- Removing a warp -Warps.remove_warp(warp_id) - -]] -function Warps.remove_warp(warp_id) - local warp = WrapData:get(warp_id) - local force_name = warp.force_name - Warps.remove_warp_tag(warp_id) - Warps.remove_warp_area(warp_id) - WrapData:remove(warp_id) - table.remove_element(force_warps[force_name], warp_id) -end - ---[[-- Update the name and icon for a warp -@tparam string warp_id the uid of the warp that you want to update -@tparam[opt] string new_name the new name that you want the warp to have -@tparam[opt] string new_icon the new icon that you want the warp to have -@tparam[opt='server'] string player_name the name of the player that made the edit - -@usage-- Changing the name and icon for a warp -Warps.update_warp(warp_id, 'My Warp', 'iron-plate', game.player.name) - -]] -function Warps.update_warp(warp_id, new_name, new_icon, player_name) - WrapData:update(warp_id, function(_, warp) - -- If the icon is not valid then replace with the old icon - if new_icon and not new_icon.name or not new_icon.type then - new_icon = warp.icon - end - - warp.last_edit_name = player_name or "" - warp.last_edit_time = game.tick - warp.name = new_name or warp.name - warp.icon = new_icon or warp.icon - end) -end - ---[[-- Set the editing state for a player, can be used as a warning or to display a text field -@tparam string warp_id the uid of the warp that you want to effect -@tparam string player_name the name of the player you want to set the state for -@tparam boolean state the new state to set editing to - -@usage-- Setting your editing state to true -Warps.set_editing(warp_id, game.player.name, true) - -]] -function Warps.set_editing(warp_id, player_name, state) - WrapData:update(warp_id, function(_, warp) - warp.currently_editing[player_name] = state - end) -end - ---[[-- Adds an update handler for when a warp is added, removed, or updated -@tparam function handler the handler which is called when a warp is updated - -@usage-- Add a game print when a warp is updated -Warps.on_update(function(warp) - game.print(warp.force_name..' now has the warp: '..warp.name) -end) - -]] -function Warps.on_update(handler) - WrapData:on_update(handler) -end - ---- Getters. --- function used to get information about warps --- @section getters - ---[[-- Gets the warp information that is linked with this id -@tparam string warp_id the uid of the warp you want to get -@treturn table the warp information - -@usage-- Getting warp information outside of on_update -local warp = Warps.get_warp(warp_id) - -]] -function Warps.get_warp(warp_id) - return WrapData:get(warp_id) -end - ---[[-- Gets all the warp ids that a force has -@tparam string force_name the name of the force that you want the warp ids for -@treturn table an array of all the warp ids - -@usage-- Getting the warp ids for a force -local warp_ids = Warps.get_force_warp_ids(game.player.force.name) - -]] -function Warps.get_force_warp_ids(force_name) - return force_warps[force_name] or {} -end - ---[[-- Get the id of the spawn warp -@tparam string force_name the name of the force that you want to get the spawn warp for -@treturn ?string|nil the uid of the spawn warp for this force if there is one - -@usage-- Getting the spawn warp id -local spawn_id = Warps.get_spawn_warp_id(game.player.force.name) - -]] -function Warps.get_spawn_warp_id(force_name) - local warp_ids = force_warps[force_name] or {} - return warp_ids.spawn -end - ---[[-- Gets the editing state for a player -@tparam string warp_id the uid of the warp you want to check -@tparam string player_name the name of the player that you want to check -@treturn boolean weather the player is currently editing this warp - -@usage-- Check if a player is editing a warp or not -local editing = Warps.get_editing(warp_id, game.player.name) - -]] -function Warps.get_editing(warp_id, player_name) - local warp = WrapData:get(warp_id) - return warp.currently_editing[player_name] -end - --- Module return -return Warps diff --git a/exp_legacy/module/modules/gui/warp-list.lua b/exp_legacy/module/modules/gui/warp-list.lua deleted file mode 100644 index 5c456558..00000000 --- a/exp_legacy/module/modules/gui/warp-list.lua +++ /dev/null @@ -1,909 +0,0 @@ ---[[-- Gui Module - Warp List - - Adds a warp list gui which allows players to add and remove warp points - @gui Warps-List - @alias warp_list -]] - -local ExpUtil = require("modules/exp_util") -local Gui = require("modules/exp_gui") -local Datastore = require("modules.exp_legacy.expcore.datastore") --- @dep expcore.datastore -local Storage = require("modules/exp_util/storage") -local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event -local Roles = require("modules/exp_roles") -local Colors = require("modules/exp_util/include/color") -local config = require("modules.exp_legacy.config.gui.warps") --- @dep config.gui.warps -local Warps = require("modules.exp_legacy.modules.control.warps") --- @dep modules.control.warps - -local format_time = ExpUtil.format_time_factory_locale{ format = "short", hours = true, minutes = true } - ---- Stores all data for the warp gui -local WrapGuiData = Datastore.connect("WrapGuiData") -WrapGuiData:set_serializer(Datastore.name_serializer) -local PlayerInRange = WrapGuiData:combine("PlayerInRange") -PlayerInRange:set_default(false) -local PlayerCooldown = WrapGuiData:combine("PlayerCooldown") -PlayerCooldown:set_default(0) - ---- Table that stores a boolean value of weather to keep the warp gui open -local keep_gui_open = {} -Storage.register(keep_gui_open, function(tbl) - keep_gui_open = tbl -end) - ---- Styles used for sprite buttons -local Styles = { - sprite22 = { height = 22, width = 22, padding = -2 }, - sprite32 = { height = 32, width = 32, left_margin = 1 }, -} ---- Status icon of a warp -local warp_status_icons = { - cooldown = "[img=utility/multiplayer_waiting_icon]", - not_available = "[img=utility/set_bar_slot]", - bypass = "[img=utility/side_menu_bonus_icon]", - current = "[img=utility/side_menu_map_icon]", - connected = "[img=utility/logistic_network_panel_white]", - different = "[img=utility/warning_white]", -} - ---- Returns if a player is allowed to edit the given warp ---- If a player is allowed to use the edit buttons -local function check_player_permissions(player, action, warp) - -- Check if the action is allow edit and then check bypass settings - if action == "allow_edit_warp" then - -- Check if the warp is the spawn then it cant be edited - local spawn_id = Warps.get_spawn_warp_id(player.force.name) - if spawn_id == warp.warp_id then - return false - end - - -- Check if the player being the last to edit will override existing permisisons - if config.user_can_edit_own_warps and warp.last_edit_name == player.name then - return true - end - end - - -- Check player has permission based on value in the config - local action_config = config[action] - if action_config == "all" then - return true - elseif action_config == "admin" then - return player.admin - elseif action_config == "exp_roles" then - return Roles.player_has_permission(player, config["exp_roles_" .. action]) - end - - -- Return false as all other conditions have not been met - return false -end - ---- Will add a new warp to the list, checks if the player is too close to an existing one --- @element add_new_warp -local add_new_warp = Gui.define("add_new_warp") - :draw{ - type = "sprite-button", - sprite = "utility/add", - tooltip = { "warp-list.add-tooltip" }, - style = "shortcut_bar_button", - name = Gui.from_name, - } - :style(Styles.sprite22) - :on_click(function(def, player, element) - if player.controller_type ~= defines.controllers.character then return end - -- Add the new warp - local force_name = player.force.name - local surface = player.physical_surface - local position = player.physical_position - - -- Check if the warp is too close to water - local water_tiles = surface.find_tiles_filtered{ collision_mask = "water_tile", radius = config.standard_proximity_radius + 1, position = position } - if #water_tiles > 0 then - player.print( - { "expcore-commands.command-fail", { "warp-list.too-close-to-water", config.standard_proximity_radius + 1 } }, - { color = Colors.orange_red } - ) - if game.player then game.player.play_sound{ path = "utility/wire_pickup" } end - for _, tile in pairs(water_tiles) do - rendering.draw_sprite{ - sprite = "utility/rail_path_not_possible", - x_scale = 0.5, - y_scale = 0.5, - target = tile.position, - surface = surface, - players = { player }, - time_to_live = 60, - } - end - - return - end - - -- Check if there are player entities in the way (has a bigger radius because the enities that can be placed by a player are larger) - local entities = surface.find_entities_filtered{ - radius = config.standard_proximity_radius + 2.5, - position = position, - collision_mask = { - "item", "object", "player", "water_tile", - }, - } - -- Remove 1 because that is the current player - if #entities > 1 then - player.print( - { "expcore-commands.command-fail", { "warp-list.too-close-to-entities", config.standard_proximity_radius + 2.5 } }, - { color = Colors.orange_red } - ) - if game.player then game.player.play_sound{ path = "utility/wire_pickup" } end - local character = player.character - for _, entity in pairs(entities) do - if entity ~= character then - rendering.draw_sprite{ - sprite = "utility/rail_path_not_possible", - x_scale = 0.5, - y_scale = 0.5, - target = entity, - surface = surface, - players = { player }, - time_to_live = 60, - } - end - end - - return - end - - -- Create the warp - local warp_id = Warps.add_warp(force_name, surface, position, player.name) - Warps.make_warp_tag(warp_id) - Warps.make_warp_area(warp_id) - end) - ---- Warp icon button, this will trigger a warp when the player is able to --- @element warp_icon_button -local warp_icon_button = Gui.define("warp_icon_button") - :draw(function(def, parent, warp) - local warp_position = warp.position - - -- The SpritePath type is not the same as the SignalID type - local sprite = warp.icon.type .. "/" .. warp.icon.name - if warp.icon.type == "virtual" then - sprite = "virtual-signal/" .. warp.icon.name - end - - -- Draw the element - return parent.add{ - type = "sprite-button", - sprite = sprite, - name = def.name, - tooltip = { "warp-list.goto-tooltip", warp_position.x, warp_position.y }, - style = "slot_button", - } - end) - :style(Styles.sprite32) - :on_click(function(def, player, element) - if element.type == "choose-elem-button" then return end - local warp_id = assert(element.parent).caption - Warps.teleport_player(warp_id, player) - - -- Reset the warp cooldown if the player does not have unlimited warps - if not check_player_permissions(player, "bypass_warp_cooldown") then - PlayerCooldown:set(player, config.update_smoothing * config.cooldown_duration) - end - - PlayerInRange:set(player, warp_id) - end) - ---- The button that is visible when the warp is in edit state --- @element warp_icon_editing -local warp_icon_editing = Gui.define("warp_icon_editing") - :draw(function(def, parent, warp) - return parent.add{ - name = def.name, - type = "choose-elem-button", - elem_type = "signal", - signal = { type = warp.icon.type, name = warp.icon.name }, - tooltip = { "warp-list.goto-edit" }, - } - end) - :style(Styles.sprite32) - ---- Warp label, visible if the player is not in edit state --- @element warp_label -local warp_label = Gui.define("warp_label") - :draw(function(def, parent, warp) - local last_edit_name = warp.last_edit_name - local last_edit_time = warp.last_edit_time - -- Draw the element - return parent.add{ - type = "label", - caption = warp.name, - tooltip = { "warp-list.last-edit", last_edit_name, format_time(last_edit_time) }, - name = def.name, - } - end) - :style{ - single_line = true, - left_padding = 2, - right_padding = 2, - horizontally_stretchable = true, - } - :on_click(function(def, player, element) - local warp_id = assert(element.parent).caption - local warp = Warps.get_warp(warp_id) - player.set_controller{ type = defines.controllers.remote, position = warp.position, surface = warp.surface } - end) - ---- Warp status, visible if the player is not in edit state ---- This will show if the warp is connected or not --- @element warp_status -local warp_status = Gui.define("warp_status") - :draw{ - type = "label", - caption = "[img=utility/electricity_icon_unplugged]", -- Temporary icon - name = Gui.from_name, - } - :style{ - -- When editing mode because textbox is larger the icon would move up. - top_padding = 1, - single_line = false, - } - ---- Warp textfield, visible if the player is in edit state --- @element warp_textfield -local warp_textfield = Gui.define("warp_textfield") - :draw(function(def, parent, warp) - -- Draw the element - return parent.add{ - type = "textfield", - text = warp.name, - clear_and_focus_on_right_click = true, - name = def.name, - } - end) - :style{ - -- Required fields to make it squashable and strechable. - minimal_width = 10, - maximal_width = 300, - horizontally_squashable = true, - horizontally_stretchable = true, - -- Other styling - height = 22, - padding = -2, - left_margin = 2, - right_margin = 2, - } - :on_confirmed(function(def, player, element) - local warp_id = assert(element.parent).caption - local warp_name = element.text - local warp_icon = assert(element.parent).parent["icon-" .. warp_id][warp_icon_editing.name].elem_value --[[@as SignalID]] - if warp_icon.type == nil then warp_icon.type = "item" end - Warps.set_editing(warp_id, player.name) - Warps.update_warp(warp_id, warp_name, warp_icon, player.name) - end) - ---- Confirms the edit to name or icon of the warp --- @element confirm_edit_button -local confirm_edit_button = Gui.define("confirm_edit_button") - :draw{ - type = "sprite-button", - sprite = "utility/confirm_slot", - tooltip = { "warp-list.confirm-tooltip" }, - style = "shortcut_bar_button_green", - name = Gui.from_name, - } - :style(Styles.sprite22) - :on_click(function(def, player, element) - local parent = assert(element.parent) - local grandparent = assert(parent.parent) - local warp_id = parent.caption --[[@as string]] - local name_flow = grandparent["name-" .. warp_id] - local icon_flow = grandparent["icon-" .. warp_id] - local warp_name = name_flow[warp_textfield.name].text - local warp_icon = icon_flow[warp_icon_editing.name].elem_value --[[@as SignalID]] - if warp_icon.type == nil then warp_icon.type = "item" end - Warps.set_editing(warp_id, player.name) - Warps.update_warp(warp_id, warp_name, warp_icon, player.name) - end) - ---- Cancels the editing changes of the selected warp name or icon --- @element cancel_edit_button -local cancel_edit_button = Gui.define("cancel_edit_button") - :draw{ - type = "sprite-button", - sprite = "utility/close_black", - tooltip = { "warp-list.cancel-tooltip" }, - style = "shortcut_bar_button_red", - name = Gui.from_name, - } - :style(Styles.sprite22) - :on_click(function(def, player, element) - local warp_id = assert(element.parent).caption - -- Check if this is the first edit, if so remove the warp. - local warp = Warps.get_warp(warp_id) - if warp.updates == 1 then - Warps.remove_warp(warp_id) - return - end - Warps.set_editing(warp_id, player.name) - end) - ---- Removes a warp from the list, including the physical area and map tag --- @element remove_warp_button -local remove_warp_button = Gui.define("remove_warp_button") - :draw{ - type = "sprite-button", - sprite = "utility/trash", - tooltip = { "warp-list.remove-tooltip" }, - style = "shortcut_bar_button_red", - name = Gui.from_name, - } - :style(Styles.sprite22) - :on_click(function(def, player, element) - local warp_id = assert(element.parent).caption - Warps.remove_warp(warp_id) - end) - ---- Opens edit mode for the warp --- @element edit_warp_button -local edit_warp_button = Gui.define("edit_warp_button") - :draw{ - type = "sprite-button", - sprite = "utility/rename_icon", - tooltip = { "warp-list.edit-tooltip-none" }, - style = "shortcut_bar_button", - name = Gui.from_name, - } - :style(Styles.sprite22) - :on_click(function(def, player, element) - local warp_id = assert(element.parent).caption - Warps.set_editing(warp_id, player.name, true) - end) - -local update_all_warp_elements ---- Set of three elements which make up each row of the warp table --- @element add_warp_elements -local add_warp_elements = Gui.define("add_warp_elements") - :draw(function(_, parent, warp) - -- Add icon flow, this will contain the warp button and warp icon edit button - local icon_flow = parent.add{ - name = "icon-" .. warp.warp_id, - type = "flow", - caption = warp.warp_id, - } - icon_flow.style.padding = 0 - - -- Add the button and the icon edit button - warp_icon_button(icon_flow, warp) - warp_icon_editing(icon_flow, warp) - - -- Add name flow, this will contain the warp label and textbox - local name_flow = parent.add{ - type = "flow", - name = "name-" .. warp.warp_id, - caption = warp.warp_id, - } - name_flow.style.padding = 0 - - -- Add the label and textfield of the warp - warp_status(name_flow) - warp_label(name_flow, warp) - warp_textfield(name_flow, warp) - - -- Add button flow, this will contain buttons to manage this specific warp - local button_flow = parent.add{ - type = "flow", - name = "button-" .. warp.warp_id, - caption = warp.warp_id, - } - button_flow.style.padding = 0 - - -- Add both edit state buttons - confirm_edit_button(button_flow) - cancel_edit_button(button_flow) - edit_warp_button(button_flow) - remove_warp_button(button_flow) - - return Gui.no_return() - end) - --- Removes the three elements that are added as part of the warp base -local function remove_warp_elements(parent, warp_id) - Gui.destroy_if_valid(parent["icon-" .. warp_id]) - Gui.destroy_if_valid(parent["name-" .. warp_id]) - Gui.destroy_if_valid(parent["button-" .. warp_id]) -end - ---- This timer controls when a player is able to warp, eg every 60 seconds --- @element warp_timer -local warp_timer = Gui.define("warp_timer") - :draw{ - type = "progressbar", - name = Gui.from_name, - tooltip = { "warp-list.timer-tooltip-zero", config.cooldown_duration }, - minimum_value = 0, - maximum_value = config.cooldown_duration * config.update_smoothing, - } - :style{ - horizontally_stretchable = true, - color = Colors.light_blue, - } - -local warp_list_container - --- Helper function to style and enable or disable a button element -local function update_warp_elements(element, warp, warp_player_is_on, on_cooldown, bypass_warp_proximity) - -- Check if button element is valid - if not element or not element.valid then return end - - local label_style = assert(element.parent).parent["name-" .. warp.warp_id][warp_label.name].style - local warp_status_element = assert(element.parent).parent["name-" .. warp.warp_id][warp_status.name] - - -- If player is not on a warp - if not warp_player_is_on then - -- If player is allowed to warp without being on a warp. If not then disable the warp location - if bypass_warp_proximity then - local position = warp.position - element.tooltip = { "warp-list.goto-bypass", position.x, position.y } - element.enabled = true - warp_status_element.tooltip = { "warp-list.goto-bypass", position.x, position.y } - warp_status_element.caption = warp_status_icons.bypass - label_style.font = "default-semibold" - else - element.tooltip = { "warp-list.goto-disabled" } - element.enabled = false - warp_status_element.tooltip = { "warp-list.goto-disabled" } - warp_status_element.caption = warp_status_icons.not_available - label_style.font = "default" - end - -- If player is on the warp that is being updated - elseif warp_player_is_on.warp_id == warp.warp_id then - element.tooltip = { "warp-list.goto-same-warp" } - element.enabled = false - warp_status_element.tooltip = { "warp-list.goto-same-warp" } - warp_status_element.caption = warp_status_icons.current - label_style.font = "default" - -- If player is on cooldown - elseif on_cooldown then - element.tooltip = { "warp-list.goto-cooldown" } - element.enabled = false - warp_status_element.tooltip = { "warp-list.goto-cooldown" } - warp_status_element.caption = warp_status_icons.cooldown - label_style.font = "default" - else - -- If the warp the player is standing on is the same as the warp that is being updated - local warp_electric_network_id = warp.electric_pole and warp.electric_pole.electric_network_id or -1 - local player_warp_electric_network_id = warp_player_is_on.electric_pole and warp_player_is_on.electric_pole.electric_network_id or -2 - if warp_electric_network_id == player_warp_electric_network_id then - local position = warp.position - element.tooltip = { "warp-list.goto-tooltip", position.x, position.y } - element.enabled = true - warp_status_element.tooltip = { "warp-list.goto-tooltip", position.x, position.y } - warp_status_element.caption = warp_status_icons.connected - label_style.font = "default-semibold" - -- If the warp is not on the same network but the player is allowed to warp without being on a warp - elseif bypass_warp_proximity then - local position = warp.position - element.tooltip = { "warp-list.goto-bypass-different-network", position.x, position.y } - element.enabled = true - warp_status_element.tooltip = { "warp-list.goto-bypass-different-network", position.x, position.y } - warp_status_element.caption = warp_status_icons.bypass - label_style.font = "default-semibold" - -- If the warp is on a different network than the one the player is standing on - else - element.tooltip = { "warp-list.goto-different-network" } - element.enabled = false - warp_status_element.tooltip = { "warp-list.goto-different-network" } - warp_status_element.caption = warp_status_icons.different - label_style.font = "default" - end - end -end - ---- Update the warp buttons for a player -function update_all_warp_elements(player, timer, warp_id) - -- Get the warp table - local container = Gui.get_left_element(warp_list_container, player) - local scroll_table = container.frame.scroll.table - - -- Check if the player is currently on cooldown - timer = timer or PlayerCooldown:get(player) - local on_cooldown = timer > 0 - -- Get the warp the player is on - warp_id = warp_id or PlayerInRange:get(player) - local warp_player_is_on = warp_id and Warps.get_warp(warp_id) or nil - -- Check player permission - local bypass_warp_proximity = check_player_permissions(player, "bypass_warp_proximity") - - -- Change the enabled state of the warp buttons - local warp_ids = Warps.get_force_warp_ids(player.force.name) - for _, next_warp_id in pairs(warp_ids) do - local element = scroll_table["icon-" .. next_warp_id][warp_icon_button.name] - local next_warp = Warps.get_warp(next_warp_id) - update_warp_elements(element, next_warp, warp_player_is_on, on_cooldown, bypass_warp_proximity) - end -end - ---- Updates a warp for a player -local function update_warp(player, warp_table, warp_id) - local warp = Warps.get_warp(warp_id) - - -- If the warp no longer exists then remove the warp elements from the warp table - if not warp then - remove_warp_elements(warp_table, warp_id) - return - end - - -- Create the warp elements if they do not already exist - if not warp_table["icon-" .. warp_id] then - add_warp_elements(warp_table, warp) - end - local icon_flow = warp_table["icon-" .. warp_id] - local name_flow = warp_table["name-" .. warp_id] - local button_flow = warp_table["button-" .. warp_id] - - -- Create local references to the elements for this warp - local warp_icon_element = icon_flow[warp_icon_button.name] - local warp_icon_edit_element = icon_flow[warp_icon_editing.name] - - local label_element = name_flow[warp_label.name] - local textfield_element = name_flow[warp_textfield.name] - - local cancel_edit_element = button_flow[cancel_edit_button.name] - local confirm_edit_element = button_flow[confirm_edit_button.name] - - local edit_warp_element = button_flow[edit_warp_button.name] - local remove_warp_element = button_flow[remove_warp_button.name] - - -- Hide the edit button if the player is not allowed to edit the warp - local player_allowed_edit = check_player_permissions(player, "allow_edit_warp", warp) - local players_editing = table.get_keys(warp.currently_editing) - edit_warp_element.visible = player_allowed_edit - - -- Set the tooltip of the edit button - if #players_editing > 0 then - edit_warp_element.hovered_sprite = "utility/warning_icon" - edit_warp_element.tooltip = { "warp-list.edit-tooltip", table.concat(players_editing, ", ") } - else - edit_warp_element.hovered_sprite = edit_warp_element.sprite - edit_warp_element.tooltip = { "warp-list.edit-tooltip-none" } - end - - -- Set the visibility of the warp elements based on whether the user is editing or not - local player_is_editing = warp.currently_editing[player.name] - if player_is_editing then - -- Set the icon elements visibility - warp_icon_element.visible = false - warp_icon_edit_element.visible = true - -- Set the name elements visibility - label_element.visible = false - textfield_element.visible = true - textfield_element.focus() - warp_table.parent.scroll_to_element(textfield_element, "top-third") - -- Set the edit buttons - cancel_edit_element.visible = true - confirm_edit_element.visible = true - -- Set the warp buttons - edit_warp_element.visible = false - remove_warp_element.visible = false - else - -- Set the icon elements visibility - warp_icon_element.visible = true - -- The SpritePath type is not the same as the SignalID type - local sprite = warp.icon.type .. "/" .. warp.icon.name - if warp.icon.type == "virtual" then - sprite = "virtual-signal/" .. warp.icon.name - end - warp_icon_element.sprite = sprite - -- Set icon edit to the warps icon - warp_icon_edit_element.elem_value = warp.icon - warp_icon_edit_element.visible = false - -- Set the name elements visibility - label_element.visible = true - label_element.caption = warp.name - textfield_element.visible = false - textfield_element.text = warp.name - -- Set the edit buttons - cancel_edit_element.visible = false - confirm_edit_element.visible = false - -- Set the warp buttons - edit_warp_element.visible = true and player_allowed_edit - remove_warp_element.visible = true and player_allowed_edit - end - - local timer = PlayerCooldown:get(player) - local current_warp_id = PlayerInRange:get(player) - local to_warp = current_warp_id and Warps.get_warp(current_warp_id) or nil - local bypass_warp_proximity = check_player_permissions(player, "bypass_warp_proximity") - update_warp_elements(warp_icon_element, warp, to_warp, timer > 0, bypass_warp_proximity) -end - --- Update all the warps for a player -local function update_all_warps(player, warp_table) - local warp_ids = Warps.get_force_warp_ids(player.force.name) - warp_table.clear() - for _, warp_id in ipairs(warp_ids) do - update_warp(player, warp_table, warp_id) - end -end - --- Update all warps for all players on a force -local function update_all_warp_force(force) - local warp_ids = Warps.get_force_warp_ids(force.name) - for _, player in pairs(force.connected_players) do - local container = Gui.get_left_element(warp_list_container, player) - local warp_table = container.frame.scroll.table - - warp_table.clear() -- Needed to re-sort the warps - for _, warp_id in ipairs(warp_ids) do - update_warp(player, warp_table, warp_id) - end - end -end - ---- Main warp list container for the left flow --- @element warp_list_container -warp_list_container = Gui.define("warp_list_container") - :draw(function(def, parent) - local player = Gui.get_player(parent) - -- Check if user has permission to add warps - local allow_add_warp = check_player_permissions(player, "allow_add_warp") - - -- Draw the internal container - local container = Gui.elements.container(parent, allow_add_warp and 268 or 220) - - -- Draw the header - local header = Gui.elements.header(container, { - name = "header", - caption = { "warp-list.main-caption" }, - tooltip = { - "warp-list.sub-tooltip", - config.cooldown_duration, - config.standard_proximity_radius, - { "warp-list.sub-tooltip-current", warp_status_icons.current }, - { "warp-list.sub-tooltip-connected", warp_status_icons.connected }, - { "warp-list.sub-tooltip-different", warp_status_icons.different }, - { "warp-list.sub-tooltip-cooldown", warp_status_icons.cooldown }, - { "warp-list.sub-tooltip-not_available", warp_status_icons.not_available }, - { "warp-list.sub-tooltip-bypass", warp_status_icons.bypass }, - }, - }) - - -- Draw the new warp button - local add_new_warp_element = add_new_warp(header) - add_new_warp_element.visible = allow_add_warp - - -- Draw the scroll table for the warps - local scroll_table = Gui.elements.scroll_table(container, 250, 3, "scroll") - -- Set the scroll panel to always show the scrollbar (not doing this will result in a changing gui size) - assert(scroll_table.parent).vertical_scroll_policy = "always" - - -- Change the style of the scroll table - local scroll_table_style = scroll_table.style - scroll_table_style.top_cell_padding = 3 - scroll_table_style.bottom_cell_padding = 3 - - -- Draw the warp cooldown progress bar - local warp_timer_element = warp_timer(container) - - -- Change the progress of the warp timer - local timer = PlayerCooldown:get(player) - if timer > 0 then - warp_timer_element.tooltip = { "warp-list.timer-tooltip", math.floor(timer / config.update_smoothing) } - warp_timer_element.value = 1 - (timer / config.update_smoothing / config.cooldown_duration) - else - warp_timer_element.tooltip = { "warp-list.timer-tooltip-zero", config.cooldown_duration } - warp_timer_element.value = 1 - end - - -- Add any existing warps - update_all_warps(player, scroll_table) - - -- Return the external container - return container.parent - end) - ---- Add the element to the left flow with a toolbar button -Gui.add_left_element(warp_list_container, false) -Gui.toolbar.create_button{ - name = "warp_list_toggle", - left_element = warp_list_container, - sprite = config.default_icon.type .. "/" .. config.default_icon.name, - tooltip = { "warp-list.main-tooltip" }, - visible = function(player, element) - return Roles.player_has_permission(player, "exp_scenario.gui.warp_list") - end -}:on_click(function(def, player, element) - -- Set gui keep open state for player that clicked the button: true if visible, false if invisible - keep_gui_open[player.name] = Gui.toolbar.get_button_toggled_state(def, player) -end) - ---- When the name of a warp is updated this is triggered -Warps.on_update(function(_, warp, old_warp) - -- Get the force to update, warp is nil when removed - if warp then - update_all_warp_force(game.forces[warp.force_name]) - else - update_all_warp_force(game.forces[old_warp.force_name]) - end -end) - ---- When the player leaves or enters range of a warp this is triggered -PlayerInRange:on_update(function(player_name, warp_id) - local player = game.players[player_name] - - -- Change if the frame is visible based on if the player is in range - if not keep_gui_open[player.name] then - Gui.toolbar.set_left_element_visible_state(warp_list_container, player, warp_id ~= nil) - end - - update_all_warp_elements(player, nil, warp_id) -end) - ---- Update the warp cooldown progress bars to match the current cooldown -PlayerCooldown:on_update(function(player_name, player_cooldown) - -- Get the progress bar element - local player = game.players[player_name] - local container = Gui.get_left_element(warp_list_container, player) - local warp_timer_element = container.frame[warp_timer.name] - - -- Set the progress - if player_cooldown and player_cooldown > 0 then - warp_timer_element.tooltip = { "warp-list.timer-tooltip", math.floor(player_cooldown / config.update_smoothing) } - warp_timer_element.value = 1 - (player_cooldown / config.update_smoothing / config.cooldown_duration) - else - warp_timer_element.tooltip = { "warp-list.timer-tooltip-zero", config.cooldown_duration } - warp_timer_element.value = 1 - end - - -- Trigger update of buttons if cooldown is now 0 - if player_cooldown == 0 then - update_all_warp_elements(player, player_cooldown, nil) - end -end) - ---- Handles updating the timer and checking distance from a warp -local r2 = config.standard_proximity_radius ^ 2 -local rs2 = config.spawn_proximity_radius ^ 2 -local mr2 = config.minimum_distance ^ 2 -Event.on_nth_tick(math.floor(60 / config.update_smoothing), function() - PlayerCooldown:update_all(function(_, player_cooldown) - if player_cooldown > 0 then return player_cooldown - 1 end - end) - - local force_warps = {} - local warps = {} - for _, player in pairs(game.connected_players) do - local was_in_range = PlayerInRange:get(player) - - -- Get the ids of all the warps on the players force - local force_name = player.force.name - local warp_ids = force_warps[force_name] - if not warp_ids then - warp_ids = Warps.get_force_warp_ids(force_name) - force_warps[force_name] = warp_ids - end - - -- Check if the force has any warps - local closest_warp = nil --- @type { warp_id: string, name: string }? - local closest_distance = nil - if #warp_ids > 0 then - local surface = player.surface - local pos = player.physical_position - local px, py = pos.x, pos.y - - -- Loop over each warp - for _, warp_id in ipairs(warp_ids) do - -- Check if warp id is cached - local warp = warps[warp_id] - if not warp then - warp = Warps.get_warp(warp_id) - warps[warp_id] = warp - end - - -- Check if the player is within range - local warp_pos = warp.position - if warp.surface == surface then - local dx, dy = px - warp_pos.x, py - warp_pos.y - local dist = (dx * dx) + (dy * dy) - if closest_distance == nil or dist < closest_distance then - closest_warp = warp - closest_distance = dist - if dist < r2 then break end - end - end - end - - -- Check the dist to the closest warp - local in_range = closest_warp ~= nil and (closest_warp.warp_id == warp_ids.spawn and closest_distance < rs2 or closest_distance < r2) - if was_in_range and not in_range then - PlayerInRange:set(player, nil) - elseif not was_in_range and in_range then - --- @cast closest_warp -nil - PlayerInRange:set(player, closest_warp.warp_id) - end - - -- Change the enabled state of the add warp button - local container = Gui.get_left_element(warp_list_container, player) - local add_warp_element = container.frame.header[add_new_warp.name] - local old_closest_warp_name = add_warp_element.tooltip[2] or closest_warp and closest_warp.name - local was_able_to_make_warp = add_warp_element.enabled - local can_make_warp = closest_distance == nil or closest_distance > mr2 - if can_make_warp and not was_able_to_make_warp then - add_warp_element.enabled = true - add_warp_element.tooltip = { "warp-list.add-tooltip" } - elseif not can_make_warp and was_able_to_make_warp or closest_warp and (old_closest_warp_name ~= closest_warp.name) then - --- @cast closest_warp -nil - add_warp_element.enabled = false - add_warp_element.tooltip = { "warp-list.too-close", closest_warp.name } - end - end - end -end) - ---- When a player is created make sure that there is a spawn warp created -Event.add(defines.events.on_player_created, function(event) - -- If the force has no spawn then make a spawn warp - local player = game.players[event.player_index] - local force = player.force --[[@as LuaForce]] - local spawn_id = Warps.get_spawn_warp_id(force.name) - if not spawn_id then - local spawn_position = force.get_spawn_position(player.surface) - spawn_id = Warps.add_warp(force.name, player.surface, spawn_position, nil, "Spawn") - Warps.set_spawn_warp(spawn_id, force) - Warps.make_warp_tag(spawn_id) - - local entities = player.surface.find_entities_filtered{ type = "electric-pole", position = spawn_position, radius = 20, limit = 1 } - if entities and entities[1] then - local warp = Warps.get_warp(spawn_id) - warp.electric_pole = entities[1] - end - end -end) - ---- Update the warps when the player joins -Event.add(defines.events.on_player_joined_game, function(event) - local player = game.players[event.player_index] - local container = Gui.get_left_element(warp_list_container, player) - local scroll_table = container.frame.scroll.table - update_all_warps(player, scroll_table) -end) - ---- Makes sure the right buttons are present when roles change -local function role_update_event(event) - local player = game.players[event.player_index] - local frame = Gui.get_left_element(warp_list_container, player).frame - - -- Check if user has permission to add warps - local allow_add_warp = check_player_permissions(player, "allow_add_warp") - -- Update container size depending on whether the player is allowed to add warps - assert(frame.parent).style.width = allow_add_warp and 268 or 220 - - -- Update the warps, in case the user can now edit them - local scroll_table = frame.scroll.table - update_all_warps(player, scroll_table) - - -- Update the new warp button in case the user can now add them - local add_new_warp_element = frame.header[add_new_warp.name] - add_new_warp_element.visible = allow_add_warp -end - -Event.add(Roles.events.on_player_roles_changed, role_update_event) - ---- When a chart tag is removed or edited make sure it is not one that belongs to a warp -local function maintain_tag(event) - if not event.player_index then return end - local tag = event.tag - local force_name = event.force.name - local warp_ids = Warps.get_force_warp_ids(force_name) - for _, warp_id in pairs(warp_ids) do - local warp = Warps.get_warp(warp_id) - local warp_tag = warp.tag - if not warp_tag or not warp_tag.valid or warp_tag == tag then - if event.name == defines.events.on_chart_tag_removed then - warp.tag = nil - end - Warps.make_warp_tag(warp_id) - end - end -end - -Event.add(defines.events.on_chart_tag_modified, maintain_tag) -Event.add(defines.events.on_chart_tag_removed, maintain_tag) diff --git a/exp_scenario/module/control.lua b/exp_scenario/module/control.lua index 1cd47d66..62c4e943 100644 --- a/exp_scenario/module/control.lua +++ b/exp_scenario/module/control.lua @@ -83,5 +83,6 @@ add(require("modules/exp_scenario/gui/rocket_info")) add(require("modules/exp_scenario/gui/science_production")) add(require("modules/exp_scenario/gui/surveillance")) add(require("modules/exp_scenario/gui/task_list")) +add(require("modules/exp_scenario/gui/warp_list")) add(require("modules/exp_scenario/gui/debug/shim")) add(require("modules/exp_scenario/gui/debug/event_view")) diff --git a/exp_scenario/module/gui/warp_list.lua b/exp_scenario/module/gui/warp_list.lua new file mode 100644 index 00000000..f76faed4 --- /dev/null +++ b/exp_scenario/module/gui/warp_list.lua @@ -0,0 +1,1218 @@ +--[[-- Gui - Warp List +Adds a list of warp points which players can travel between, add, and edit +]] + +local Gui = require("modules/exp_gui") +local Roles = require("modules/exp_roles") +local Colors = require("modules/exp_util/include/color") +local config = require("modules.exp_legacy.config.gui.warps") + +local ExpUtil = require("modules/exp_util") +local format_time = ExpUtil.format_time_factory_locale{ format = "short", hours = true, minutes = true } + +local floor = math.floor +local ceil = math.ceil + +--- @class ExpGui_WarpList.elements +local Elements = {} + +local Styles = { + sprite22 = { height = 22, width = 22, padding = -2 }, + sprite32 = { height = 32, width = 32, left_margin = 1 }, +} + +--- Rich text icons shown next to a warp name to describe why it can or can not be used +local status_icons = { + cooldown = "[img=utility/multiplayer_waiting_icon]", + not_available = "[img=utility/set_bar_slot]", + bypass = "[img=utility/side_menu_bonus_icon]", + current = "[img=utility/side_menu_map_icon]", + connected = "[img=utility/logistic_network_panel_white]", + different = "[img=utility/warning_white]", +} + +local update_interval = floor(60 / config.update_smoothing) +local cooldown_ticks = config.cooldown_duration * 60 +local proximity_radius_sq = config.standard_proximity_radius ^ 2 +local spawn_radius_sq = config.spawn_proximity_radius ^ 2 +local minimum_distance_sq = config.minimum_distance ^ 2 + +--- Names of the entities which make up a warp area, used to find them when the area is removed +local area_entity_names = {} --- @type string[] +for _, entity in ipairs(config.entities) do + area_entity_names[#area_entity_names + 1] = entity[1] +end + +--- @class ExpGui_WarpList.Warp +--- @field id number +--- @field force LuaForce +--- @field name string +--- @field icon SignalID +--- @field surface LuaSurface +--- @field position MapPosition.struct The tile the warp is centred on +--- @field last_user LuaPlayer? Nil for warps made by the scenario +--- @field last_edit_tick number +--- @field editing table Names of the players editing the warp keyed by their index +--- @field tag LuaCustomChartTag? +--- @field electric_pole LuaEntity? Pole within the warp area, warps on the same network can be travelled between +--- @field old_tile string? The tile the warp area replaced, nil when no area was made +--- @field new boolean? True until the first edit is confirmed, discarding that edit removes the warp + +--- @class ExpGui_WarpList.PlayerStatus +--- @field in_range ExpGui_WarpList.Warp? The warp the player is standing on +--- @field on_cooldown boolean +--- @field bypass_proximity boolean + +--- Check a permission from the config, which says who an action applies to +--- @param player LuaPlayer +--- @param action "bypass_warp_cooldown" | "bypass_warp_proximity" | "allow_add_warp" | "allow_edit_warp" +--- @return boolean +local function has_config_permission(player, action) + local setting = config[action] + if setting == "all" then + return true + elseif setting == "admin" then + return player.admin + elseif setting == "exp_roles" then + return Roles.player_has_permission(player, config["exp_roles_" .. action]) + end + + return false +end + +--- Check if a player can edit a warp, the spawn warp can never be edited +--- @param player LuaPlayer +--- @param warp ExpGui_WarpList.Warp +--- @return boolean +local function has_permission_edit_warp(player, warp) + if Elements.container.get_spawn_warp(warp.force) == warp then + return false + end + + if config.user_can_edit_own_warps and warp.last_user == player then + return true + end + + return has_config_permission(player, "allow_edit_warp") +end + +--- The sprite path for a warp icon, signal ids and sprite paths name virtual signals differently +--- @param icon SignalID +--- @return SpritePath +local function icon_sprite(icon) + if icon.type == "virtual" then + return "virtual-signal/" .. icon.name + end + return (icon.type or "item") .. "/" .. icon.name +end + +--- Draw a cross over something that stops a warp being added +--- @param surface LuaSurface +--- @param player LuaPlayer +--- @param target LuaEntity | MapPosition +local function draw_blocker(surface, player, target) + rendering.draw_sprite{ + sprite = "utility/rail_path_not_possible", + x_scale = 0.5, + y_scale = 0.5, + target = target, + surface = surface, + players = { player }, + time_to_live = 60, + } +end + +--- Button in the header which adds a warp at the player +--- @class ExpGui_WarpList.elements.add_warp_button: ExpElement +--- @overload fun(parent: LuaGuiElement): LuaGuiElement +Elements.add_warp_button = Gui.define("warp_list/add_warp_button") + :track_all_elements() + :draw{ + type = "sprite-button", + sprite = "utility/add", + tooltip = { "exp-gui_warp-list.tooltip-add" }, + style = "shortcut_bar_button", + } + :style(Styles.sprite22) + :on_click(function(_, player) + if player.controller_type ~= defines.controllers.character then return end + local surface = player.physical_surface + local position = player.physical_position + + local water_tiles = surface.find_tiles_filtered{ + collision_mask = "water_tile", + radius = config.standard_proximity_radius + 1, + position = position, + } + if #water_tiles > 0 then + player.print({ "exp-gui_warp-list.add-warning-water", config.standard_proximity_radius + 1 }, Colors.orange_red) + player.play_sound{ path = "utility/wire_pickup" } + for _, tile in pairs(water_tiles) do + draw_blocker(surface, player, tile.position) + end + return + end + + -- A larger radius than the area because the entities a player can place are larger than tiles + local entities = surface.find_entities_filtered{ + radius = config.standard_proximity_radius + 2.5, + position = position, + collision_mask = { "item", "object", "player", "water_tile" }, + } + if #entities > 1 then -- The player's own character is always found + player.print({ "exp-gui_warp-list.add-warning-entities", config.standard_proximity_radius + 2.5 }, Colors.orange_red) + player.play_sound{ path = "utility/wire_pickup" } + local character = player.character + for _, entity in pairs(entities) do + if entity ~= character then + draw_blocker(surface, player, entity) + end + end + return + end + + local force = player.force --[[@as LuaForce]] + local warp = Elements.container.add_warp(force, surface, position, player) + Elements.container.set_editing(warp, player, true) + end) --[[@as any]] + +--- Refresh whether the add button is enabled based on the closest warp +--- @param player LuaPlayer +--- @param blocking ExpGui_WarpList.Warp? The warp too close to add another, nil when a warp can be added +function Elements.add_warp_button.refresh_player_blocking(player, blocking) + local tooltip = blocking and { "exp-gui_warp-list.tooltip-add-too-close", blocking.name } or { "exp-gui_warp-list.tooltip-add" } + for _, add_warp_button in Elements.add_warp_button:tracked_elements(player) do + add_warp_button.enabled = blocking == nil + add_warp_button.tooltip = tooltip + end +end + +--- @class ExpGui_WarpList.elements.warp_button.element_data +--- @field warp ExpGui_WarpList.Warp + +--- Button which travels to the warp, enabled when the player is allowed to use it +--- @class ExpGui_WarpList.elements.warp_button: ExpElement +--- @field data table +--- @overload fun(parent: LuaGuiElement, warp: ExpGui_WarpList.Warp): LuaGuiElement +Elements.warp_button = Gui.define("warp_list/warp_button") + :draw(function(_, parent, warp) + --- @cast warp ExpGui_WarpList.Warp + return parent.add{ + type = "sprite-button", + sprite = icon_sprite(warp.icon), + style = "slot_button", + } + end) + :style(Styles.sprite32) + :element_data{ + warp = Gui.from_argument(1), + } + :on_click(function(def, player, warp_button) + --- @cast def ExpGui_WarpList.elements.warp_button + Elements.container.teleport_player(player, def.data[warp_button].warp) + end) --[[@as any]] + +--- @class ExpGui_WarpList.elements.icon_picker.element_data +--- @field warp ExpGui_WarpList.Warp + +--- Signal picker shown in place of the warp button while editing +--- @class ExpGui_WarpList.elements.icon_picker: ExpElement +--- @field data table +--- @overload fun(parent: LuaGuiElement, warp: ExpGui_WarpList.Warp): LuaGuiElement +Elements.icon_picker = Gui.define("warp_list/icon_picker") + :draw(function(_, parent, warp) + --- @cast warp ExpGui_WarpList.Warp + return parent.add{ + type = "choose-elem-button", + elem_type = "signal", + signal = warp.icon, + tooltip = { "exp-gui_warp-list.tooltip-icon" }, + } + end) + :style(Styles.sprite32) + :element_data{ + warp = Gui.from_argument(1), + } --[[@as any]] + +--- Get the icon chosen in a picker, the game omits the type for items +--- @param icon_picker LuaGuiElement +--- @return SignalID +function Elements.icon_picker.get_icon(icon_picker) + local icon = icon_picker.elem_value --[[@as SignalID?]] + if not icon then + return Elements.icon_picker.data[icon_picker].warp.icon + end + icon.type = icon.type or "item" + return icon +end + +--- Icon describing whether the warp can be used +--- @class ExpGui_WarpList.elements.status_label: ExpElement +--- @overload fun(parent: LuaGuiElement): LuaGuiElement +Elements.status_label = Gui.define("warp_list/status_label") + :draw{ + type = "label", + caption = status_icons.not_available, + } + :style{ + top_padding = 1, -- Keeps the icon in place when the taller textfield is shown + single_line = false, + } --[[@as any]] + +--- @class ExpGui_WarpList.elements.name_label.element_data +--- @field warp ExpGui_WarpList.Warp + +--- Warp name, clicking it opens the map at the warp +--- @class ExpGui_WarpList.elements.name_label: ExpElement +--- @field data table +--- @overload fun(parent: LuaGuiElement, warp: ExpGui_WarpList.Warp): LuaGuiElement +Elements.name_label = Gui.define("warp_list/name_label") + :draw(function(_, parent, warp) + --- @cast warp ExpGui_WarpList.Warp + return parent.add{ + type = "label", + caption = warp.name, + } + end) + :style{ + single_line = true, + left_padding = 2, + right_padding = 2, + horizontally_stretchable = true, + } + :element_data{ + warp = Gui.from_argument(1), + } + :on_click(function(def, player, name_label) + --- @cast def ExpGui_WarpList.elements.name_label + local warp = def.data[name_label].warp + player.set_controller{ type = defines.controllers.remote, position = warp.position, surface = warp.surface } + end) --[[@as any]] + +--- Refresh the name and last edit of the label +--- @param name_label LuaGuiElement +--- @param warp ExpGui_WarpList.Warp +function Elements.name_label.refresh(name_label, warp) + local last_user = warp.last_user and warp.last_user.name or "" + name_label.caption = warp.name + name_label.tooltip = { "exp-gui_warp-list.tooltip-last-edit", last_user, format_time(warp.last_edit_tick) } +end + +--- @class ExpGui_WarpList.elements.name_field.element_data +--- @field warp ExpGui_WarpList.Warp +--- @field icon_picker LuaGuiElement + +--- Textfield shown in place of the name label while editing, confirming it saves the edit +--- @class ExpGui_WarpList.elements.name_field: ExpElement +--- @field data table +--- @overload fun(parent: LuaGuiElement, warp: ExpGui_WarpList.Warp, icon_picker: LuaGuiElement): LuaGuiElement +Elements.name_field = Gui.define("warp_list/name_field") + :draw(function(_, parent, warp) + --- @cast warp ExpGui_WarpList.Warp + return parent.add{ + type = "textfield", + text = warp.name, + clear_and_focus_on_right_click = true, + } + end) + :style{ + minimal_width = 10, + maximal_width = 300, + horizontally_squashable = true, + horizontally_stretchable = true, + height = 22, + padding = -2, + left_margin = 2, + right_margin = 2, + } + :element_data{ + warp = Gui.from_argument(1), + icon_picker = Gui.from_argument(2), + } + :on_confirmed(function(def, player, name_field) + --- @cast def ExpGui_WarpList.elements.name_field + local elements = def.data[name_field] + local icon = Elements.icon_picker.get_icon(elements.icon_picker) + Elements.container.update_warp(elements.warp, name_field.text, icon, player) + end) --[[@as any]] + +--- @class ExpGui_WarpList.elements.confirm_edit_button.element_data +--- @field warp ExpGui_WarpList.Warp +--- @field name_field LuaGuiElement +--- @field icon_picker LuaGuiElement + +--- Button which saves the name and icon being edited +--- @class ExpGui_WarpList.elements.confirm_edit_button: ExpElement +--- @field data table +--- @overload fun(parent: LuaGuiElement, warp: ExpGui_WarpList.Warp, name_field: LuaGuiElement, icon_picker: LuaGuiElement): LuaGuiElement +Elements.confirm_edit_button = Gui.define("warp_list/confirm_edit_button") + :draw{ + type = "sprite-button", + sprite = "utility/confirm_slot", + tooltip = { "exp-gui_warp-list.tooltip-confirm" }, + style = "shortcut_bar_button_green", + } + :style(Styles.sprite22) + :element_data{ + warp = Gui.from_argument(1), + name_field = Gui.from_argument(2), + icon_picker = Gui.from_argument(3), + } + :on_click(function(def, player, confirm_edit_button) + --- @cast def ExpGui_WarpList.elements.confirm_edit_button + local elements = def.data[confirm_edit_button] + local icon = Elements.icon_picker.get_icon(elements.icon_picker) + Elements.container.update_warp(elements.warp, elements.name_field.text, icon, player) + end) --[[@as any]] + +--- @class ExpGui_WarpList.elements.discard_edit_button.element_data +--- @field warp ExpGui_WarpList.Warp + +--- Button which discards an edit, a warp which was never confirmed is removed +--- @class ExpGui_WarpList.elements.discard_edit_button: ExpElement +--- @field data table +--- @overload fun(parent: LuaGuiElement, warp: ExpGui_WarpList.Warp): LuaGuiElement +Elements.discard_edit_button = Gui.define("warp_list/discard_edit_button") + :draw{ + type = "sprite-button", + sprite = "utility/close_black", + tooltip = { "exp-gui_warp-list.tooltip-discard" }, + style = "shortcut_bar_button_red", + } + :style(Styles.sprite22) + :element_data{ + warp = Gui.from_argument(1), + } + :on_click(function(def, player, discard_edit_button) + --- @cast def ExpGui_WarpList.elements.discard_edit_button + local warp = def.data[discard_edit_button].warp + if warp.new then + Elements.container.remove_warp(warp) + else + Elements.container.set_editing(warp, player, false) + end + end) --[[@as any]] + +--- @class ExpGui_WarpList.elements.edit_warp_button.element_data +--- @field warp ExpGui_WarpList.Warp + +--- Button which starts editing a warp +--- @class ExpGui_WarpList.elements.edit_warp_button: ExpElement +--- @field data table +--- @overload fun(parent: LuaGuiElement, warp: ExpGui_WarpList.Warp): LuaGuiElement +Elements.edit_warp_button = Gui.define("warp_list/edit_warp_button") + :draw{ + type = "sprite-button", + sprite = "utility/rename_icon", + tooltip = { "exp-gui_warp-list.tooltip-edit-none" }, + style = "shortcut_bar_button", + } + :style(Styles.sprite22) + :element_data{ + warp = Gui.from_argument(1), + } + :on_click(function(def, player, edit_warp_button) + --- @cast def ExpGui_WarpList.elements.edit_warp_button + Elements.container.set_editing(def.data[edit_warp_button].warp, player, true) + end) --[[@as any]] + +--- Refresh the tooltip which lists who is editing the warp +--- @param edit_warp_button LuaGuiElement +--- @param warp ExpGui_WarpList.Warp +function Elements.edit_warp_button.refresh(edit_warp_button, warp) + if next(warp.editing) then + local player_names = table.get_values(warp.editing) + edit_warp_button.hovered_sprite = "utility/warning_icon" + edit_warp_button.tooltip = { "exp-gui_warp-list.tooltip-edit", table.concat(player_names, ", ") } + else + edit_warp_button.hovered_sprite = "utility/rename_icon" + edit_warp_button.tooltip = { "exp-gui_warp-list.tooltip-edit-none" } + end +end + +--- @class ExpGui_WarpList.elements.remove_warp_button.element_data +--- @field warp ExpGui_WarpList.Warp + +--- Button which removes a warp along with its area and map tag +--- @class ExpGui_WarpList.elements.remove_warp_button: ExpElement +--- @field data table +--- @overload fun(parent: LuaGuiElement, warp: ExpGui_WarpList.Warp): LuaGuiElement +Elements.remove_warp_button = Gui.define("warp_list/remove_warp_button") + :draw{ + type = "sprite-button", + sprite = "utility/trash", + tooltip = { "exp-gui_warp-list.tooltip-remove" }, + style = "shortcut_bar_button_red", + } + :style(Styles.sprite22) + :element_data{ + warp = Gui.from_argument(1), + } + :on_click(function(def, _, remove_warp_button) + --- @cast def ExpGui_WarpList.elements.remove_warp_button + Elements.container.remove_warp(def.data[remove_warp_button].warp) + end) --[[@as any]] + +--- @class ExpGui_WarpList.elements.warp_table.row +--- @field warp ExpGui_WarpList.Warp +--- @field icon_flow LuaGuiElement +--- @field name_flow LuaGuiElement +--- @field action_flow LuaGuiElement +--- @field warp_button LuaGuiElement +--- @field icon_picker LuaGuiElement +--- @field status_label LuaGuiElement +--- @field name_label LuaGuiElement +--- @field name_field LuaGuiElement +--- @field confirm_edit_button LuaGuiElement +--- @field discard_edit_button LuaGuiElement +--- @field edit_warp_button LuaGuiElement +--- @field remove_warp_button LuaGuiElement + +--- @class ExpGui_WarpList.elements.warp_table.element_data +--- @field rows table Keyed by warp id + +--- Table with a row of icon, name, and actions for each warp of the player's force +--- @class ExpGui_WarpList.elements.warp_table: ExpElement +--- @field data table +--- @overload fun(parent: LuaGuiElement): LuaGuiElement +Elements.warp_table = Gui.define("warp_list/warp_table") + :track_all_elements() + :element_data{ rows = {} } + :draw(function(_, parent) + local warp_table = Gui.elements.scroll_table(parent, 250, 3, "scroll") + -- Always showing the scrollbar stops the gui changing width as warps are added + assert(warp_table.parent).vertical_scroll_policy = "always" + return warp_table + end) + :style{ + top_cell_padding = 3, + bottom_cell_padding = 3, + } --[[@as any]] + +--- Add a row for a warp +--- @param warp_table LuaGuiElement +--- @param warp ExpGui_WarpList.Warp +--- @return ExpGui_WarpList.elements.warp_table.row +function Elements.warp_table.add_row(warp_table, warp) + local icon_flow = warp_table.add{ type = "flow" } + icon_flow.style.padding = 0 + local warp_button = Elements.warp_button(icon_flow, warp) + local icon_picker = Elements.icon_picker(icon_flow, warp) + + local name_flow = warp_table.add{ type = "flow" } + name_flow.style.padding = 0 + local status_label = Elements.status_label(name_flow) + local name_label = Elements.name_label(name_flow, warp) + local name_field = Elements.name_field(name_flow, warp, icon_picker) + + local action_flow = warp_table.add{ type = "flow" } + action_flow.style.padding = 0 + local confirm_edit_button = Elements.confirm_edit_button(action_flow, warp, name_field, icon_picker) + local discard_edit_button = Elements.discard_edit_button(action_flow, warp) + local edit_warp_button = Elements.edit_warp_button(action_flow, warp) + local remove_warp_button = Elements.remove_warp_button(action_flow, warp) + + local row = { + warp = warp, + icon_flow = icon_flow, + name_flow = name_flow, + action_flow = action_flow, + warp_button = warp_button, + icon_picker = icon_picker, + status_label = status_label, + name_label = name_label, + name_field = name_field, + confirm_edit_button = confirm_edit_button, + discard_edit_button = discard_edit_button, + edit_warp_button = edit_warp_button, + remove_warp_button = remove_warp_button, + } + + Elements.warp_table.data[warp_table].rows[warp.id] = row + return row +end + +--- Remove the row for a warp +--- @param warp_table LuaGuiElement +--- @param warp ExpGui_WarpList.Warp +function Elements.warp_table.remove_row(warp_table, warp) + local rows = Elements.warp_table.data[warp_table].rows + local row = rows[warp.id] + if not row then return end + rows[warp.id] = nil + row.icon_flow.destroy() + row.name_flow.destroy() + row.action_flow.destroy() +end + +--- Refresh whether a warp can be used from where the player is +--- @param row ExpGui_WarpList.elements.warp_table.row +--- @param status ExpGui_WarpList.PlayerStatus +function Elements.warp_table._refresh_row_status(row, status) + local warp = row.warp + local in_range = status.in_range + local position = warp.position + local tooltip, icon, enabled + + if not in_range then + if status.bypass_proximity then + tooltip, icon, enabled = { "exp-gui_warp-list.tooltip-goto-bypass", position.x, position.y }, status_icons.bypass, true + else + tooltip, icon, enabled = { "exp-gui_warp-list.tooltip-goto-disabled" }, status_icons.not_available, false + end + elseif in_range == warp then + tooltip, icon, enabled = { "exp-gui_warp-list.tooltip-goto-same-warp" }, status_icons.current, false + elseif status.on_cooldown then + tooltip, icon, enabled = { "exp-gui_warp-list.tooltip-goto-cooldown" }, status_icons.cooldown, false + else + -- Warps are connected when their poles share an electric network, missing poles never match + local network_id = warp.electric_pole and warp.electric_pole.valid and warp.electric_pole.electric_network_id or -1 + local in_range_network_id = in_range.electric_pole and in_range.electric_pole.valid and in_range.electric_pole.electric_network_id or -2 + if network_id == in_range_network_id then + tooltip, icon, enabled = { "exp-gui_warp-list.tooltip-goto", position.x, position.y }, status_icons.connected, true + elseif status.bypass_proximity then + tooltip, icon, enabled = { "exp-gui_warp-list.tooltip-goto-bypass-different-network", position.x, position.y }, status_icons.bypass, true + else + tooltip, icon, enabled = { "exp-gui_warp-list.tooltip-goto-different-network" }, status_icons.different, false + end + end + + row.warp_button.tooltip = tooltip + row.warp_button.enabled = enabled + row.status_label.tooltip = tooltip + row.status_label.caption = icon + row.name_label.style.font = enabled and "default-semibold" or "default" +end + +--- Refresh a row to match its warp, the player's permissions, and where the player is +--- @param warp_table LuaGuiElement +--- @param row ExpGui_WarpList.elements.warp_table.row +--- @param status ExpGui_WarpList.PlayerStatus +function Elements.warp_table.refresh_row(warp_table, row, status) + local player = Gui.get_player(warp_table) + local warp = row.warp + local can_edit = has_permission_edit_warp(player, warp) + local editing = warp.editing[player.index] ~= nil + + Elements.edit_warp_button.refresh(row.edit_warp_button, warp) + Elements.name_label.refresh(row.name_label, warp) + + row.warp_button.visible = not editing + row.icon_picker.visible = editing + row.name_label.visible = not editing + row.name_field.visible = editing + row.confirm_edit_button.visible = editing + row.discard_edit_button.visible = editing + row.edit_warp_button.visible = can_edit and not editing + row.remove_warp_button.visible = can_edit and not editing + + if editing then + row.name_field.focus() + assert(warp_table.parent).scroll_to_element(row.name_field, "top-third") + else + row.warp_button.sprite = icon_sprite(warp.icon) + row.icon_picker.elem_value = warp.icon + row.name_field.text = warp.name + end + + Elements.warp_table._refresh_row_status(row, status) +end + +--- Rebuild every row in the order of the warps +--- @param warp_table LuaGuiElement +--- @param warps ExpGui_WarpList.Warp[] +--- @param status ExpGui_WarpList.PlayerStatus +function Elements.warp_table.rebuild(warp_table, warps, status) + warp_table.clear() + Elements.warp_table.data[warp_table].rows = {} + for _, warp in ipairs(warps) do + local row = Elements.warp_table.add_row(warp_table, warp) + Elements.warp_table.refresh_row(warp_table, row, status) + end +end + +--- Refresh the row of one warp for the online players of its force +--- @param warp ExpGui_WarpList.Warp +function Elements.warp_table.refresh_row_force(warp) + for player, warp_table in Elements.warp_table:online_elements(warp.force) do + --- @cast player -nil + local row = Elements.warp_table.data[warp_table].rows[warp.id] + if row then + Elements.warp_table.refresh_row(warp_table, row, Elements.container.get_status(player)) + end + end +end + +--- Refresh whether each warp can be used from where the player is +--- @param player LuaPlayer +function Elements.warp_table.refresh_status_player(player) + local status = Elements.container.get_status(player) + for _, warp_table in Elements.warp_table:tracked_elements(player) do + for _, row in pairs(Elements.warp_table.data[warp_table].rows) do + Elements.warp_table._refresh_row_status(row, status) + end + end +end + +--- Rebuild the rows for a player, used when their force or permissions change +--- @param player LuaPlayer +function Elements.warp_table.rebuild_player(player) + local warps = Elements.container.get_warps(player.force --[[@as LuaForce]]) + local status = Elements.container.get_status(player) + for _, warp_table in Elements.warp_table:tracked_elements(player) do + Elements.warp_table.rebuild(warp_table, warps, status) + end +end + +--- Rebuild the rows for the online players of a force, used when the warps change +--- @param force LuaForce +function Elements.warp_table.rebuild_force(force) + local warps = Elements.container.get_warps(force) + for player, warp_table in Elements.warp_table:online_elements(force) do + --- @cast player -nil + Elements.warp_table.rebuild(warp_table, warps, Elements.container.get_status(player)) + end +end + +--- Progress bar which fills as the warp cooldown runs out +--- @class ExpGui_WarpList.elements.cooldown_bar: ExpElement +--- @overload fun(parent: LuaGuiElement): LuaGuiElement +Elements.cooldown_bar = Gui.define("warp_list/cooldown_bar") + :track_all_elements() + :draw{ + type = "progressbar", + value = 1, + tooltip = { "exp-gui_warp-list.tooltip-cooldown-none", config.cooldown_duration }, + } + :style{ + horizontally_stretchable = true, + color = Colors.light_blue, + } --[[@as any]] + +--- Refresh the bar to show the ticks left on the cooldown +--- @param cooldown_bar LuaGuiElement +--- @param cooldown number +function Elements.cooldown_bar.refresh(cooldown_bar, cooldown) + if cooldown > 0 then + cooldown_bar.value = 1 - cooldown / cooldown_ticks + cooldown_bar.tooltip = { "exp-gui_warp-list.tooltip-cooldown", ceil(cooldown / 60) } + else + cooldown_bar.value = 1 + cooldown_bar.tooltip = { "exp-gui_warp-list.tooltip-cooldown-none", config.cooldown_duration } + end +end + +--- Refresh the bars of a player +--- @param player LuaPlayer +--- @param cooldown number +function Elements.cooldown_bar.refresh_player(player, cooldown) + for _, cooldown_bar in Elements.cooldown_bar:tracked_elements(player) do + Elements.cooldown_bar.refresh(cooldown_bar, cooldown) + end +end + +--- @class ExpGui_WarpList.elements.container.elements +--- @field frame LuaGuiElement +--- @field add_warp_button LuaGuiElement +--- @field warp_table LuaGuiElement +--- @field cooldown_bar LuaGuiElement + +--- @class ExpGui_WarpList.elements.container.force_data +--- @field warps ExpGui_WarpList.Warp[] The spawn warp first, then by name +--- @field spawn ExpGui_WarpList.Warp? +--- @field next_warp_id number + +--- @class ExpGui_WarpList.elements.container.player_data +--- @field cooldown number Ticks until the player can warp again +--- @field in_range ExpGui_WarpList.Warp? The warp the player is standing on +--- @field blocking ExpGui_WarpList.Warp? The warp too close for another to be added +--- @field keep_open boolean True once the player toggles the list themselves, which stops it hiding when they leave a warp + +--- @class ExpGui_WarpList.elements.container.data +--- @field [LuaGuiElement] ExpGui_WarpList.elements.container.elements +--- @field [LuaForce] ExpGui_WarpList.elements.container.force_data +--- @field [LuaPlayer] ExpGui_WarpList.elements.container.player_data + +--- Container added to the left gui flow +--- @class ExpGui_WarpList.elements.container: ExpElement +--- @field data ExpGui_WarpList.elements.container.data +Elements.container = Gui.define("warp_list/container") + :draw(function(def, parent) + --- @cast def ExpGui_WarpList.elements.container + local player = Gui.get_player(parent) + local allow_add_warp = has_config_permission(player, "allow_add_warp") + local container = Gui.elements.container(parent, allow_add_warp and 268 or 220) + + local header = Gui.elements.header(container, { + caption = { "exp-gui_warp-list.caption-main" }, + tooltip = { + "exp-gui_warp-list.tooltip-sub", + config.cooldown_duration, + config.standard_proximity_radius, + { "exp-gui_warp-list.tooltip-sub-current", status_icons.current }, + { "exp-gui_warp-list.tooltip-sub-connected", status_icons.connected }, + { "exp-gui_warp-list.tooltip-sub-different", status_icons.different }, + { "exp-gui_warp-list.tooltip-sub-cooldown", status_icons.cooldown }, + { "exp-gui_warp-list.tooltip-sub-not-available", status_icons.not_available }, + { "exp-gui_warp-list.tooltip-sub-bypass", status_icons.bypass }, + }, + }) + + local add_warp_button = Elements.add_warp_button(header) + add_warp_button.visible = allow_add_warp + + local warp_table = Elements.warp_table(container) + local force = player.force --[[@as LuaForce]] + Elements.warp_table.rebuild(warp_table, def.get_warps(force), def.get_status(player)) + + local cooldown_bar = Elements.cooldown_bar(container) + Elements.cooldown_bar.refresh(cooldown_bar, def._get_player_data(player).cooldown) + + local root = Gui.elements.container.get_root_element(container) + def.data[root] = { + frame = container, + add_warp_button = add_warp_button, + warp_table = warp_table, + cooldown_bar = cooldown_bar, + } + + return root + end) --[[@as any]] + +--- Get or create the data for a force +--- @param force LuaForce +--- @return ExpGui_WarpList.elements.container.force_data +function Elements.container._get_force_data(force) + local force_data = Elements.container.data[force] + if not force_data then + force_data = { warps = {}, next_warp_id = 1 } + Elements.container.data[force] = force_data + end + return force_data +end + +--- Get or create the data for a player +--- @param player LuaPlayer +--- @return ExpGui_WarpList.elements.container.player_data +function Elements.container._get_player_data(player) + local player_data = Elements.container.data[player] + if not player_data then + player_data = { cooldown = 0, keep_open = false } + Elements.container.data[player] = player_data + end + return player_data +end + +--- Get the warps of a force, the spawn warp first and then by name +--- @param force LuaForce +--- @return ExpGui_WarpList.Warp[] +function Elements.container.get_warps(force) + return Elements.container._get_force_data(force).warps +end + +--- Get the spawn warp of a force +--- @param force LuaForce +--- @return ExpGui_WarpList.Warp? +function Elements.container.get_spawn_warp(force) + return Elements.container._get_force_data(force).spawn +end + +--- Get what decides which warps a player can use +--- @param player LuaPlayer +--- @return ExpGui_WarpList.PlayerStatus +function Elements.container.get_status(player) + local player_data = Elements.container._get_player_data(player) + return { + in_range = player_data.in_range, + on_cooldown = player_data.cooldown > 0, + bypass_proximity = has_config_permission(player, "bypass_warp_proximity"), + } +end + +--- Keep the warps of a force in display order +--- @param force_data ExpGui_WarpList.elements.container.force_data +function Elements.container._sort_warps(force_data) + local spawn = force_data.spawn + table.sort(force_data.warps, function(a, b) + if a == spawn then return true end + if b == spawn then return false end + if a.name == b.name then return a.id < b.id end + return a.name < b.name + end) +end + +--- Add or update the map tag of a warp +--- @param warp ExpGui_WarpList.Warp +function Elements.container._make_tag(warp) + local tag = warp.tag + if tag and tag.valid then + tag.text = "Warp: " .. warp.name + tag.icon = warp.icon + return + end + + local position = warp.position + warp.tag = warp.force.add_chart_tag(warp.surface, { + position = { x = position.x + 0.5, y = position.y + 0.5 }, + text = "Warp: " .. warp.name, + icon = warp.icon, + }) +end + +--- Remove the map tag of a warp +--- @param warp ExpGui_WarpList.Warp +function Elements.container._remove_tag(warp) + local tag = warp.tag + if tag and tag.valid then + tag.destroy() + end + warp.tag = nil +end + +--- Place the tiles and entities which mark a warp on the ground +--- @param warp ExpGui_WarpList.Warp +function Elements.container._make_area(warp) + local surface = warp.surface + local position = warp.position + warp.old_tile = surface.get_tile(position.x, position.y).name + + local tiles = {} + for index, tile in ipairs(config.tiles) do + tiles[index] = { name = tile[1], position = { x = tile[2] + position.x, y = tile[3] + position.y } } + end + surface.set_tiles(tiles) + + for _, entity_config in ipairs(config.entities) do + local entity = assert(surface.create_entity{ + name = entity_config[1], + position = { x = entity_config[2] + position.x, y = entity_config[3] + position.y }, + force = "neutral", + }) + entity.destructible = false + entity.health = 0 + entity.minable_flag = false + entity.rotatable = false + + if entity.type == "electric-pole" then + warp.electric_pole = entity + end + end +end + +--- Restore the ground under a warp and remove its entities +--- @param warp ExpGui_WarpList.Warp +function Elements.container._remove_area(warp) + local old_tile = warp.old_tile + if not old_tile then return end + + local surface = warp.surface + local position = warp.position + local tiles = {} + for index, tile in ipairs(config.tiles) do + tiles[index] = { name = old_tile, position = { x = tile[2] + position.x, y = tile[3] + position.y } } + end + surface.set_tiles(tiles) + + local radius = config.standard_proximity_radius + local area = { + left_top = { x = position.x - radius, y = position.y - radius }, + right_bottom = { x = position.x + radius, y = position.y + radius }, + } + local entities = surface.find_entities_filtered{ force = "neutral", area = area, name = area_entity_names } + for _, entity in pairs(entities) do + if entity.destructible == false then + entity.destructible = true + entity.die(entity.force) + end + end + + -- The area may not be covered by a radar + warp.force.chart(surface, area) +end + +--- Add a warp for a force, when added by a player it is new until they confirm their first edit +--- @param force LuaForce +--- @param surface LuaSurface +--- @param position MapPosition.struct +--- @param player LuaPlayer? +--- @param name string? +--- @return ExpGui_WarpList.Warp +function Elements.container.add_warp(force, surface, position, player, name) + local force_data = Elements.container._get_force_data(force) + local id = force_data.next_warp_id + force_data.next_warp_id = id + 1 + + --- @type ExpGui_WarpList.Warp + local warp = { + id = id, + force = force, + name = name or "New warp", + icon = { type = config.default_icon.type, name = config.default_icon.name }, + surface = surface, + position = { x = floor(position.x), y = floor(position.y) }, + last_user = player, + last_edit_tick = game.tick, + editing = {}, + } + + -- Warps added by a player start in edit mode, discarding that edit removes them again + if player then + warp.new = true + Elements.container._make_area(warp) + end + + force_data.warps[#force_data.warps + 1] = warp + Elements.container._sort_warps(force_data) + Elements.container._make_tag(warp) + + Elements.warp_table.rebuild_force(force) + return warp +end + +--- Remove a warp along with its area and map tag +--- @param warp ExpGui_WarpList.Warp +function Elements.container.remove_warp(warp) + local force_data = Elements.container._get_force_data(warp.force) + table.remove_element(force_data.warps, warp) + if force_data.spawn == warp then + force_data.spawn = nil + end + + Elements.container._remove_tag(warp) + Elements.container._remove_area(warp) + + -- Offline players are corrected by their next proximity update + for _, player in pairs(warp.force.connected_players) do + local player_data = Elements.container._get_player_data(player) + if player_data.in_range == warp then player_data.in_range = nil end + if player_data.blocking == warp then player_data.blocking = nil end + end + + for _, warp_table in Elements.warp_table:online_elements(warp.force) do + Elements.warp_table.remove_row(warp_table, warp) + end +end + +--- Save a new name and icon for a warp, which also ends the player's edit +--- @param warp ExpGui_WarpList.Warp +--- @param name string +--- @param icon SignalID +--- @param player LuaPlayer +function Elements.container.update_warp(warp, name, icon, player) + warp.name = name + warp.icon = icon + warp.last_user = player + warp.last_edit_tick = game.tick + warp.editing[player.index] = nil + warp.new = nil + + Elements.container._sort_warps(Elements.container._get_force_data(warp.force)) + Elements.container._make_tag(warp) + Elements.warp_table.rebuild_force(warp.force) +end + +--- Start or stop a player editing a warp +--- @param warp ExpGui_WarpList.Warp +--- @param player LuaPlayer +--- @param editing boolean +function Elements.container.set_editing(warp, player, editing) + warp.editing[player.index] = editing and player.name or nil + Elements.warp_table.refresh_row_force(warp) +end + +--- Make a warp the spawn of its force, the force respawns there +--- @param warp ExpGui_WarpList.Warp +function Elements.container.set_spawn_warp(warp) + local force_data = Elements.container._get_force_data(warp.force) + force_data.spawn = warp + Elements.container._sort_warps(force_data) + warp.force.set_spawn_position(warp.position, warp.surface) +end + +--- Move a player to a warp and start their cooldown +--- @param player LuaPlayer +--- @param warp ExpGui_WarpList.Warp +function Elements.container.teleport_player(player, warp) + local surface = warp.surface + local position = { x = warp.position.x + 0.5, y = warp.position.y + 0.5 } + + local vehicle = player.vehicle + if vehicle then + local vehicle_position = surface.find_non_colliding_position(vehicle.name, position, 32, 1) + -- Only cars can be teleported between surfaces + if vehicle.type == "car" then + vehicle.teleport(vehicle_position, surface) + elseif surface == vehicle.surface then + if not vehicle.teleport(vehicle_position) then + player.driving = false + player.teleport(surface.find_non_colliding_position("character", position, 32, 1), surface) + end + end + else + player.teleport(surface.find_non_colliding_position("character", position, 32, 1), surface) + end + + local player_data = Elements.container._get_player_data(player) + if not has_config_permission(player, "bypass_warp_cooldown") then + player_data.cooldown = cooldown_ticks + Elements.cooldown_bar.refresh_player(player, cooldown_ticks) + end + player_data.in_range = warp + Elements.warp_table.refresh_status_player(player) +end + +--- Refresh the parts of a player's gui which depend on their permissions +--- @param player LuaPlayer +function Elements.container.refresh_player(player) + local allow_add_warp = has_config_permission(player, "allow_add_warp") + for _, container in Elements.container:tracked_elements(player) do + local elements = Elements.container.data[container] + elements.frame.style.minimal_width = allow_add_warp and 268 or 220 + elements.add_warp_button.visible = allow_add_warp + end + Elements.warp_table.rebuild_player(player) +end + +--- Count down the cooldown of a player +--- @param player LuaPlayer +--- @param player_data ExpGui_WarpList.elements.container.player_data +function Elements.container._update_cooldown(player, player_data) + if player_data.cooldown == 0 then return end + local cooldown = player_data.cooldown - update_interval + if cooldown < 0 then cooldown = 0 end + player_data.cooldown = cooldown + + Elements.cooldown_bar.refresh_player(player, cooldown) + if cooldown == 0 then + Elements.warp_table.refresh_status_player(player) + end +end + +--- Find which warp a player is standing on and whether one can be added where they are +--- @param player LuaPlayer +--- @param player_data ExpGui_WarpList.elements.container.player_data +function Elements.container._update_proximity(player, player_data) + local force_data = Elements.container._get_force_data(player.force --[[@as LuaForce]]) + local surface = player.physical_surface + local position = player.physical_position + local px, py = position.x, position.y + + local closest, closest_distance = nil, nil --- @type ExpGui_WarpList.Warp?, number? + for _, warp in ipairs(force_data.warps) do + if warp.surface == surface then + local dx, dy = px - warp.position.x, py - warp.position.y + local distance = dx * dx + dy * dy + if not closest_distance or distance < closest_distance then + closest, closest_distance = warp, distance + end + end + end + + local in_range = nil --- @type ExpGui_WarpList.Warp? + if closest and closest_distance < (closest == force_data.spawn and spawn_radius_sq or proximity_radius_sq) then + in_range = closest + end + if in_range ~= player_data.in_range then + player_data.in_range = in_range + if not player_data.keep_open then + Gui.toolbar.set_left_element_visible_state(Elements.container, player, in_range ~= nil) + end + Elements.warp_table.refresh_status_player(player) + end + + local blocking = nil --- @type ExpGui_WarpList.Warp? + if closest and closest_distance <= minimum_distance_sq then + blocking = closest + end + if blocking ~= player_data.blocking then + player_data.blocking = blocking + Elements.add_warp_button.refresh_player_blocking(player, blocking) + end +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_warp_list", + left_element = Elements.container, + sprite = config.default_icon.type .. "/" .. config.default_icon.name, + tooltip = { "exp-gui_warp-list.tooltip-main" }, + visible = function(player, element) + return Roles.player_has_permission(player, "exp_scenario.gui.warp_list") + end +}:on_click(function(def, player) + -- A list the player opened themselves stays open when they leave a warp + Elements.container._get_player_data(player).keep_open = Gui.toolbar.get_button_toggled_state(def, player) +end) + +--- Give a force a spawn warp when its first player is created +--- @param event EventData.on_player_created +local function on_player_created(event) + local player = Gui.get_player(event) + local force = player.force --[[@as LuaForce]] + if Elements.container.get_spawn_warp(force) then return end + + local surface = player.physical_surface + local position = force.get_spawn_position(surface) + local warp = Elements.container.add_warp(force, surface, position, nil, "Spawn") + Elements.container.set_spawn_warp(warp) + + -- Spawn has no warp area, so any pole nearby connects it to the network + local poles = surface.find_entities_filtered{ type = "electric-pole", position = position, radius = 20, limit = 1 } + warp.electric_pole = poles[1] +end + +--- Rebuild the gui for a player as it may be outdated +--- @param event EventData.on_player_joined_game | EventData.on_player_changed_force +local function refresh_player_warps(event) + local player = Gui.get_player(event) + Elements.container.refresh_player(player) + Elements.cooldown_bar.refresh_player(player, Elements.container._get_player_data(player).cooldown) +end + +--- Rebuild the gui for a player as their permissions may have changed +--- @param event EventData.ExpRoles.on_player_roles_changed +local function refresh_player_permissions(event) + Elements.container.refresh_player(Gui.get_player(event)) +end + +--- Restore the map tag of a warp when a player edits or removes it +--- @param event EventData.on_chart_tag_modified | EventData.on_chart_tag_removed +local function on_chart_tag_changed(event) + if not event.player_index then return end + for _, warp in ipairs(Elements.container.get_warps(event.force)) do + local tag = warp.tag + if not tag or not tag.valid or tag == event.tag then + if event.name == defines.events.on_chart_tag_removed then + warp.tag = nil + end + Elements.container._make_tag(warp) + end + end +end + +--- Count down cooldowns and check where each player is +local function update_players() + for _, player in pairs(game.connected_players) do + local player_data = Elements.container._get_player_data(player) + Elements.container._update_cooldown(player, player_data) + Elements.container._update_proximity(player, player_data) + end +end + +local e = defines.events + +return { + elements = Elements, + events = { + [e.on_player_created] = on_player_created, + [e.on_player_joined_game] = refresh_player_warps, + [e.on_player_changed_force] = refresh_player_warps, + [e.on_chart_tag_modified] = on_chart_tag_changed, + [e.on_chart_tag_removed] = on_chart_tag_changed, + [Roles.events.on_player_roles_changed] = refresh_player_permissions, + }, + on_nth_tick = { + [update_interval] = update_players, + } +} diff --git a/exp_scenario/module/locale/en.cfg b/exp_scenario/module/locale/en.cfg index 5dc39d3b..a4884bd1 100644 --- a/exp_scenario/module/locale/en.cfg +++ b/exp_scenario/module/locale/en.cfg @@ -490,6 +490,37 @@ caption-create-footer=Create task caption-edit-footer=Edit task caption-view-footer=Task details +[exp-gui_warp-list] +caption-main=Warp List [img=info] +tooltip-main=Warp List +tooltip-sub=Warps can only be used every __1__ seconds and when within __2__ tiles\n__3__\n__4__\n__5__\n__6__\n__7__\n__8__ +tooltip-sub-current= - __1__ This is your current warp point; +tooltip-sub-connected= - __1__ You can travel to this warp point; +tooltip-sub-different= - __1__ This warp is on a different network; +tooltip-sub-cooldown= - __1__ You are currently on cooldown; +tooltip-sub-not-available= - __1__ You are not in range of a warp point; +tooltip-sub-bypass= - __1__ Your role allows you to travel here; +tooltip-add=Add new warp +tooltip-add-too-close=Cannot create warp; too close to existing warp point: __1__ +add-warning-water=Cannot create warp; too close to water, please move __1__ tiles away from the water body or landfill it +add-warning-entities=Cannot create warp; too close to other entities, please move __1__ tiles away from the entities or remove them +tooltip-icon=Edit warp icon +tooltip-last-edit=Last edited by __1__ at __2__\nClick to view on map +tooltip-confirm=Save changes +tooltip-discard=Discard changes +tooltip-edit=Currently being edited by: __1__ +tooltip-edit-none=Currently being edited by: Nobody +tooltip-remove=Remove warp +tooltip-cooldown=Please wait __1__ seconds before you can warp +tooltip-cooldown-none=After each warp you will need to wait __1__ seconds before you can warp again +tooltip-goto=Go to x __1__ y __2__ +tooltip-goto-bypass=Go to x __1__ y __2__, bypass mode +tooltip-goto-bypass-different-network=Go to x __1__ y __2__, bypass mode different network +tooltip-goto-same-warp=You are already on this warp +tooltip-goto-different-network=This warp is on a different network, use power poles to connect it +tooltip-goto-cooldown=You are on cooldown, wait for your cooldown to recharge +tooltip-goto-disabled=You are not on a warp point, walk to a warp point + [exp_afk-kick] kick-message=All players were kicked because everyone was AFK. diff --git a/exp_scenario/module/locale/zh-CN.cfg b/exp_scenario/module/locale/zh-CN.cfg index 1b776db8..1c56589d 100644 --- a/exp_scenario/module/locale/zh-CN.cfg +++ b/exp_scenario/module/locale/zh-CN.cfg @@ -486,6 +486,37 @@ caption-edit-footer=修改工作流程 caption-view-footer=工作流程細節 tooltip-confirm = Save changes (minimum of 5 characters long) +[exp-gui_warp-list] +caption-main=傳送陣清單 [img=info] +tooltip-main=傳送陣清單 +tooltip-sub=傳送陣在 __1__ 秒內只能使用一次。 亦需在 __2__ 格之內使用\n__3__\n__4__\n__5__\n__6__\n__7__\n__8__ +tooltip-sub-current= - __1__ 你目前的傳送陣; +tooltip-sub-connected= - __1__ 你可以到本傳送陣; +tooltip-sub-different= - __1__ 該傳送陣未連接; +tooltip-sub-cooldown= - __1__ 傳送冷卻中; +tooltip-sub-not-available= - __1__ 你不在傳送陣範圍; +tooltip-sub-bypass= - __1__ 你可以傳送到這裏; +tooltip-add=新建傳送陣 +tooltip-add-too-close=無法新建傳送陣; 太接近 __1__ 了 +add-warning-water=無法新建傳送陣; 太接近水源了, 請移動 __1__ 格又或者填平該地方 +add-warning-entities=無法新建傳送陣; 太接近其他東西, 請移動 __1__ 格又或者拆除該東西 +tooltip-icon=修改傳送陣圖案 +tooltip-last-edit=最後由 __1__ 在 __2__ 更改\n按這在地圖上看 +tooltip-confirm=儲存更改 +tooltip-discard=放棄更改 +tooltip-edit=現被 __1__ 修改中 +tooltip-edit-none=現沒有被人修改 +tooltip-remove=拆除傳送陣 +tooltip-cooldown=請等 __1__ 秒才再次傳送 +tooltip-cooldown-none=你要等 __1__ 秒才可以再次傳送 +tooltip-goto=到 X __1__ Y __2__ +tooltip-goto-bypass=到 X __1__ Y __2__, 繞行模式 +tooltip-goto-bypass-different-network=到 X __1__ Y __2__, 繞行模式 (不同網絡) +tooltip-goto-same-warp=你已在該傳送陣 +tooltip-goto-different-network=該傳送陣未連接, 你可使用電纜連接 +tooltip-goto-cooldown=傳送冷卻中, 請等候冷卻 +tooltip-goto-disabled=你不在傳送陣, 請移至任意傳送陣 + [exp_afk-kick] kick-message=因地圖中沒有活躍玩家,所以所有人都已被請離。 diff --git a/exp_scenario/module/locale/zh-TW.cfg b/exp_scenario/module/locale/zh-TW.cfg index 1b776db8..1c56589d 100644 --- a/exp_scenario/module/locale/zh-TW.cfg +++ b/exp_scenario/module/locale/zh-TW.cfg @@ -486,6 +486,37 @@ caption-edit-footer=修改工作流程 caption-view-footer=工作流程細節 tooltip-confirm = Save changes (minimum of 5 characters long) +[exp-gui_warp-list] +caption-main=傳送陣清單 [img=info] +tooltip-main=傳送陣清單 +tooltip-sub=傳送陣在 __1__ 秒內只能使用一次。 亦需在 __2__ 格之內使用\n__3__\n__4__\n__5__\n__6__\n__7__\n__8__ +tooltip-sub-current= - __1__ 你目前的傳送陣; +tooltip-sub-connected= - __1__ 你可以到本傳送陣; +tooltip-sub-different= - __1__ 該傳送陣未連接; +tooltip-sub-cooldown= - __1__ 傳送冷卻中; +tooltip-sub-not-available= - __1__ 你不在傳送陣範圍; +tooltip-sub-bypass= - __1__ 你可以傳送到這裏; +tooltip-add=新建傳送陣 +tooltip-add-too-close=無法新建傳送陣; 太接近 __1__ 了 +add-warning-water=無法新建傳送陣; 太接近水源了, 請移動 __1__ 格又或者填平該地方 +add-warning-entities=無法新建傳送陣; 太接近其他東西, 請移動 __1__ 格又或者拆除該東西 +tooltip-icon=修改傳送陣圖案 +tooltip-last-edit=最後由 __1__ 在 __2__ 更改\n按這在地圖上看 +tooltip-confirm=儲存更改 +tooltip-discard=放棄更改 +tooltip-edit=現被 __1__ 修改中 +tooltip-edit-none=現沒有被人修改 +tooltip-remove=拆除傳送陣 +tooltip-cooldown=請等 __1__ 秒才再次傳送 +tooltip-cooldown-none=你要等 __1__ 秒才可以再次傳送 +tooltip-goto=到 X __1__ Y __2__ +tooltip-goto-bypass=到 X __1__ Y __2__, 繞行模式 +tooltip-goto-bypass-different-network=到 X __1__ Y __2__, 繞行模式 (不同網絡) +tooltip-goto-same-warp=你已在該傳送陣 +tooltip-goto-different-network=該傳送陣未連接, 你可使用電纜連接 +tooltip-goto-cooldown=傳送冷卻中, 請等候冷卻 +tooltip-goto-disabled=你不在傳送陣, 請移至任意傳送陣 + [exp_afk-kick] kick-message=因地圖中沒有活躍玩家,所以所有人都已被請離。