local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event local Storage = require("modules/exp_util/storage") local Gui = require("modules.exp_legacy.expcore.gui") --- @dep expcore.gui ----- Locals ----- local follow_label -- Gui constructor local following = {} local spectating = {} local Public = {} ----- Storage data ----- Storage.register({ following = following, spectating = spectating }, function(tbl) following = tbl.following spectating = tbl.spectating end) ----- Public Functions ----- --- Test if a player is in spectator mode -- @tparam LuaPlayer player The player to test the controller type of -- @treturn boolean Returns true if the player is in spectator mode function Public.is_spectating(player) assert(player and player.valid, 'Invalid player given to follower') return player.controller_type == defines.controllers.spectator end --- Puts a player into spectator mode while maintaining an association to their character -- @tparam LuaPlayer player The player that will be placed into spectator mode -- @treturn boolean Returns false if the player was already in spectator mode function Public.start_spectate(player) assert(player and player.valid, 'Invalid player given to follower') if spectating[player.index] or not player.character then return false end local character = player.character local opened = player.opened player.set_controller{ type = defines.controllers.spectator } player.associate_character(character) spectating[player.index] = character if opened then player.opened = opened end -- Maintain opened after controller change return true end --- Return a player from spectator mode back to their character, if their character was killed then respawn them -- @tparam LuaPlayer player The player that will leave spectator mode function Public.stop_spectate(player) assert(player and player.valid, 'Invalid player given to follower') local character = spectating[player.index] spectating[player.index] = nil if character and character.valid then local opened = player.opened player.teleport(character.position, character.surface) player.set_controller{ type = defines.controllers.character, character = character } if opened then player.opened = opened end -- Maintain opened after controller change else player.ticks_to_respawn = 300 end end --- Test if a player is in follow mode -- @tparam LuaPlayer player The player to test the follow mode of -- @treturn boolean Returns true if the player is in follow mode function Public.is_following(player) assert(player and player.valid, 'Invalid player given to follower') return following[player.index] ~= nil end --- Puts a player into spectator mode and follows an entity as it moves -- @tparam LuaPlayer player The player that will follow the entity -- @tparam ?LuaPlayer|LuaEntity entity The player or entity that will be followed function Public.start_follow(player, entity) assert(player and player.valid, 'Invalid player given to follower') assert(entity and entity.valid, 'Invalid entity given to follower') local spectate = Public.start_spectate(player) player.close_map() follow_label(player.gui.screen, entity) player.teleport(entity.position, entity.surface) following[player.index] = { player, entity, entity.position, spectate } end --- Returns camera control to the player, will return a player to their character if start_follow placed them into spectator mode -- @tparam LuaPlayer player The player that will regain control of their camera function Public.stop_follow(player) assert(player and player.valid, 'Invalid player given to follower') if following[player.index] and following[player.index][4] and Public.is_spectating(player) then Public.stop_spectate(player) end Gui.destroy_if_valid(player.gui.screen[follow_label.name]) following[player.index] = nil end --- Returns camera control to all players, will return a player to their character if start_follow placed them into spectator mode function Public.stop_all() for key, data in pairs(following) do Public.stop_follow(data[1]) end end ----- Gui ----- --- Label used to show that the player is following, also used to allow esc to stop following -- @element follow_label follow_label = Gui.element(function(definition, parent, target) Gui.destroy_if_valid(parent[definition.name]) local label = parent.add{ type = 'label', style = 'frame_title', caption = 'Following '..target.name..'.\nClick here or press esc to stop following.', name = definition.name } local player = Gui.get_player_from_element(parent) local res = player.display_resolution label.location = {0, res.height-150} label.style.width = res.width label.style.horizontal_align = 'center' player.opened = label return label end) :static_name(Gui.unique_static_name) :on_click(Public.stop_follow) :on_close(function(player) -- Don't call set_controller during on_close as it invalidates the controller -- Setting an invalid position (as to not equal their current) will call stop_follow on the next tick following[player.index][3] = {} end) ----- Events ----- --- Updates the location of the player as well as doing some sanity checks -- @tparam LuaPlayer player The player to update the position of -- @tparam ?LuaPlayer|LuaEntity entity The player or entity being followed local function update_player_location(player, entity, old_position) if player.character or not entity.valid then Public.stop_follow(player) elseif player.position.x ~= old_position.x or player.position.y ~= old_position.y then Public.stop_follow(player) else player.teleport(entity.position, entity.surface) end end --- Updates the locations of all players currently following something local function update_all() for _, data in pairs(following) do update_player_location(data[1], data[2], data[3]) data[3] = data[1].position end end -- Update the location of all players each tick Event.add(defines.events.on_tick, update_all) -- Check for player leaving Event.add(defines.events.on_pre_player_left_game, function(event) local player = game.players[event.player_index] Public.stop_follow(player) for _, data in pairs(following) do if data[2] == player then Public.stop_follow(data[1]) end end end) ----- Module Return ----- return Public