require("mod-gui") story_custom_handlers = {} function tip_story_init(story_table) story_init_helpers(story_table) storage.story = story_init() script.on_event(defines.events.on_tick, function(event) if not game.tick_paused then story_update(storage.story, event, nil, function() script.on_event(defines.events.on_tick, nil) return true end) end end) end function story_init_helpers(story) story_points_by_name = {} story_branches = {} for story_index, data in pairs(story) do if (story_index ~= "update-functions") then story_branches[story_index] = data for index, item in pairs(data) do if item.name ~= nil then story_points_by_name[item.name] = {} story_points_by_name[item.name].story_index = story_index story_points_by_name[item.name].position = index end end else story_update_table = data end end end function story_init(player) local result = {} -- List of names of currently active update functions result.updates = {} -- Tick, when the current state started result.current_story_started_at = 0 -- There can be different stories (sequencies), this specifies which one is active result.story_index = 1 -- Position in the current story result.story_position = 1 -- Utility variables used to ensure that the init of the story item is called just once result.init_called_last_on_story_index = 0 result.init_called_last_on_position = 0 return result end function story_update(story, event, next_level, onwin) if event.name == defines.events.on_entity_renamed then return end if event.entity then storage.last_built_position = event.entity.position end on_gui_click(event) local branches = story_branches[story.story_index] local starting_story_index = story.story_index local starting_story_position = story.story_position local branch = branches[story.story_position] if story.story_position > #branches then return end if branch.init ~= nil then if story.init_called_last_on_story_index ~= story.story_index or story.init_called_last_on_position ~= story.story_position then story.init_called_last_on_story_index = story.story_index story.init_called_last_on_position = story.story_position if branch.init(event, story) then return end if story.story_index ~= starting_story_index or story.story_position ~= starting_story_position then return end end else story.init_called_last_on_story_index = 0 story.init_called_last_on_position = 0 end for index, item in pairs(story.updates) do if story_update_table[index](event, story) then return end end if story.story_index ~= starting_story_index or story.story_position ~= starting_story_position then return end if branch.update ~= nil then branch.update(event, story) end if branch.idle ~= nil then local count = (event.tick - story.current_story_started_at)/60 if branch.idle[count] then branch.idle[count](event, story) end end if story.story_index ~= starting_story_index or story.story_position ~= starting_story_position then return end if ( branch.condition == nil and event.name == defines.events.on_tick ) or ( branch.condition ~= nil and branch.condition(event, story) ) then if story.story_index ~= starting_story_index or story.story_position ~= starting_story_position then return end if branch.action ~= nil then branch.action(event, story) end if story.story_index ~= starting_story_index or story.story_position ~= starting_story_position then return end story.story_position = story.story_position + 1 story.current_story_started_at = event.tick if story.story_position > #branches then if (onwin == nil or not onwin()) and not game.finished then if next_level then game.set_game_state{game_finished=true, player_won=true, next_level=next_level} else game.set_game_state{game_finished=true, player_won=true} end end end end end function story_add_update(story, name) story.updates[name] = true end function story_remove_update(story, name) story.updates[name] = nil end --also calls the action of the story point previous to the one you are jumping to function story_jump_to(story, name) local story_point = story_points_by_name[name] story.story_index = story_point.story_index story.story_position = story_point.position if story.story_position ~= 1 then local branch = story_branches[story.story_index] if branch[story.story_position - 1].action ~= nil then branch[story.story_position - 1].action() end end end function story_elapsed(story, event, seconds) return event.tick - story.current_story_started_at > seconds * 60 end function story_elapsed_check(seconds) return function(event, current_story) return story_elapsed(current_story, event, seconds) end end function story_show_message_dialog(param, player) if game.is_multiplayer() then if player then player.print(param.text) else game.print(param.text) end else game.show_message_dialog(param) end end function set_goal(goal_string, update_only) storage.goal_string = goal_string for k, player in pairs (game.players) do player_set_goal(player, goal_string, update_only) end end function player_set_goal(player, goal_string, update_only) local gui = player.gui.goal if goal_string and goal_string ~= "" then player.set_goal_description(goal_string, update_only) elseif update_only == false then player.set_goal_description("hi") player.set_goal_description("") else player.set_goal_description("") end end function on_player_joined(player) if storage.goal_string then player_set_goal(player, storage.goal_string) end if storage.set_info then for k, info in pairs (storage.set_info) do player_set_info(info) end end end function flash_goal() for k, player in pairs (game.players) do local goal_string = player.get_goal_description() player.set_goal_description("hi") player.set_goal_description(goal_string) end end function set_info(info) if not storage.set_info then storage.set_info = {} end if not info then storage.set_info = nil for k, player in pairs (game.players) do player_set_info(player, info) end return end if info.custom_handler then assert(story_custom_handlers[info.custom_handler]) end local append = info.append if not append then storage.set_info = {} end table.insert(storage.set_info, info) for k, player in pairs (game.players) do player_set_info(player, info) end end function player_set_info(player, info) local gui = player.gui.goal local info_flow = gui.goal_info_frame if not info then if info_flow then info_flow.destroy() end else if not info_flow then info_flow = gui.add{type = "flow", name = "goal_info_frame", direction = "vertical"} info_flow.style.vertical_spacing = 8 info_flow.style.top_padding = 8 end if not info.append then info_flow.clear() end if info then if info.custom_handler then story_custom_handlers[info.custom_handler](info_flow) end if info.text or info[1] then local label = info_flow.add { type = "label", caption = info.text or info } label.style.single_line = false end if info.picture then local flow = info_flow.add{type = "flow"} if type(info.picture) == "string" then flow.add{type = "frame", style = "deep_frame_in_shallow_frame"}.add { type = "sprite", sprite = info.picture } else local element = flow.add{type = "frame", style = "deep_frame_in_shallow_frame"}.add { type = "sprite", name = info.picture.name, tooltip = info.picture.tooltip, sprite = info.picture.path } if info.picture.lua_style then for name, value in pairs (info.picture.lua_style) do element.style[name] = value end end end end if info.pictures then local flow = info_flow.add{type = "flow"} for k, picture in pairs (info.pictures) do if type(picture) == "string" then flow.add { type = "sprite", sprite = picture } else if picture.split then flow = info_flow.add{type = "flow"} end local element = flow.add{type = "frame", style = "deep_frame_in_shallow_frame"}.add { type = "sprite", sprite = picture.path, tooltip = picture.tooltip } if picture.lua_style then for name, value in pairs (picture.lua_style) do element.style[name] = value end end end end end if info.camera then local camera = info_flow.add { type = "camera", name = info.camera.name, tooltip = info.camera.tooltip, position = info.camera.position, surface_index = info.camera.surface_index, zoom = info.camera.zoom } camera.style.minimal_width = info.camera.minimal_width or 32 camera.style.minimal_height = info.camera.minimal_height or 32 end end end end function export_entities(param) if not param then param = {} end local surface = param.surface or game.surfaces[1] local item = surface.create_entity{name = "item-on-ground", position = {-400,0}, stack = "blueprint", force = "player"} local blueprint = item.stack local entities = param.entities if not entities then if param.area then entities = surface.find_entities_filtered{area = param.area} else entities = surface.find_entities() end end local ignore = param.ignore or { player = true, particle = true, projectile = true, ["item-request-proxy"] = true, explosion = true, ["highlight-box"] = true } local get_inventory = function(entity) if not entity.valid then return nil end local inventory = {} for k = 1,10 do local inv = entity.get_inventory(k) if inv then inventory[k] = inv.get_contents() end end if #inventory > 0 then return inventory else return nil end end local exported = {} local index_map = {} local count = 1 for k, entity in pairs (entities) do if entity.valid then if entity ~= item and not (ignore[entity.type]) then local info = {} blueprint.create_blueprint{surface = surface, force = "player", area = entity.bounding_box} list = blueprint.get_blueprint_entities() if list then local this = false for i, listed in pairs (list) do if listed.name == entity.name and entity.type ~= "legacy-curved-rail" and entity.type ~= "legacy-straight-rail" then this = list[i] break end end if this then info = this end end if entity.direction and entity.direction ~= 0 then info.direction = entity.direction end info.index = count local unit_number = entity.unit_number if unit_number then index_map[unit_number] = count end info.name = entity.name if entity.type == "resource" then info.amount = entity.amount elseif entity.type == "entity-ghost" then info.inner_name = entity.ghost_name elseif entity.type == "item-entity" then info.stack = {name = entity.stack.name, count = entity.stack.count} elseif entity.type == "transport-belt" or entity.type == "underground-belt" then info.line_contents = {} for k = 1, 2 do local line = entity.get_transport_line(k) info.line_contents[k] = line.get_contents() line.clear() end elseif entity.type == "splitter" then info.line_contents = {} for k = 1, 8 do local line = entity.get_transport_line(k) info.line_contents[k] = line.get_contents() line.clear() end elseif entity.type == "locomotive" then info.schedule = entity.train.schedule info.speed = entity.train.speed info.manual_mode = entity.train.manual_mode info.direction = math.floor(0.5+entity.orientation*8)%8 elseif entity.type == "assembling-machine" then if entity.get_recipe() then info.recipe = entity.get_recipe().name end end if entity.type == "underground-belt" then info.type = entity.belt_to_ground_type end info.color = entity.color info.force = entity.force.name info.position = entity.position info.inventory = get_inventory(entity) info.backer_name = entity.backer_name info.minable = entity.minable info.rotatable = entity.rotatable info.operable = entity.operable info.destructible = entity.destructible info.entity_number = nil exported[count] = info count = count + 1 end end end for k, entity in pairs (entities) do if entity.valid and entity.unit_number then local entity_index = index_map[entity.unit_number] if entity_index then local wires = {} for _, connector in pairs(entity.get_wire_connectors()) do for _, neighbour in pairs(connector.connections) do local other_index = index_map[neighbour.owner.unit_number] if other_index then table.insert(wires, { entity_index, connector.wire_connector_id, other_index, neighbour.wire_connector_id }) end end end if #wires > 0 then exported[entity_index].wires = wires end end end end for k, entity in pairs (entities) do if entity.valid then if entity ~= item and not (ignore[entity.type]) then entity.destroy() end end end if item.valid then item.destroy() end return exported end function recreate_entities(array, param, bool) if not param then param = {} end local offset = param.offset or {0,0} local surface = param.surface or game.surfaces[1] local force = param.force or "player" local created_entities = {} local created_count = 1 local remaining = {} local remaining_count = 1 local index_map = {} local filter = param.filter local filter_map = {} if filter then for k, name in pairs (filter) do filter_map[name] = true end end for k, entity in pairs (array) do if not filter or filter_map[entity.name] then local save_position = {x = entity.position.x, y = entity.position.y} entity.position.x = entity.position.x + (offset[1] or offset.x) entity.position.y = entity.position.y + (offset[2] or offset.y) entity.force = force entity.expires = entity.expires or false if not param.check_can_place or surface.can_place_entity(entity) then if not entity.index then entity.index = -1 end local created = false if bool or (entity.name ~= "locomotive" and entity.name ~= "rail-signal") then created = surface.create_entity(entity) index_map[entity.index] = created end entity.position = save_position if created then index_map[entity.index] = created if entity.filters then for k, filter in pairs (entity.filters) do created.set_filter(filter.index, filter.name) end end if entity.amount then created.amount = entity.amount end if entity.inventory then for index, contents in pairs (entity.inventory) do local inventory = created.get_inventory(index) if inventory then for name, count in pairs (contents) do created.insert({name = name, count = count}) end end end end if entity.line_contents then for k, contents in pairs (entity.line_contents) do local line = created.get_transport_line(k) for name, count in pairs (contents) do for i = 0, count-1 do line.insert_at((0.1+i)/count, {name = name, count = 1}) end end end end if entity.backer_name then created.backer_name = entity.backer_name end if entity.color then created.color = entity.color end if entity.recipe then created.set_recipe(entity.recipe) end created.minable = entity.minable or false created.rotatable = entity.rotatable or false created.operable = entity.operable or false created.destructible = entity.destructible or false if entity.schedule then created.train.schedule = entity.schedule created.train.speed = entity.speed created.train.manual_mode = entity.manual_mode end created_entities[created_count] = created created_count = created_count + 1 else remaining[remaining_count] = entity remaining_count = remaining_count + 1 end end end end if not bool then for k, entity in pairs (recreate_entities(remaining, param, true)) do created_entities[created_count] = entity created_count = created_count + 1 end end for k, entity in pairs (array) do local created = index_map[entity.index] if created and created.valid then if entity.wires then for _, wire in pairs (entity.wires) do local first_connector = index_map[wire[1]].get_wire_connector(wire[2], true) local second_connector = index_map[wire[3]].get_wire_connector(wire[4], true) if first_connector and second_connector then first_connector.connect_to(second_connector) end end end end end return created_entities end function limit_camera(origin, distance) for k, player in pairs (game.players) do local position = player.position local new_position = {} new_position.x = math.min (position.x, origin[1] + distance) new_position.x = math.max (new_position.x, origin[1] - distance) new_position.y = math.min (position.y, origin[2] + distance) new_position.y = math.max (new_position.y, origin[2] - distance) if (new_position.x ~= position.x) or (new_position.y ~= position.y) then player.teleport(new_position) end end end function find_gui_recursive(gui, name) for k, child in pairs (gui.children) do if child.name == name then return child end local result = find_gui_recursive(child, name) if result then return result end end return false end function add_button(gui) local button = gui.add{type = "button", name = "story_continue_button", caption = {"continue"}} storage.continue = false return button end story_custom_handlers["@add_button"] = add_button story_custom_handlers["@add_finish_button"] = function(flow) add_button(flow).caption = {"finish"} end story_custom_handlers["@add_continue_button"] = function(flow) add_button(flow).caption = {"continue"} end function on_gui_click(event) if event.name ~= defines.events.on_gui_click then return end local element = event.element if not element.valid then return end local name = element.name local player = game.players[event.player_index] if name == "story_continue_button" then if not element.enabled then return end storage.continue = true set_continue_button_style(function (button) if button.valid then button.enabled = false end end) element.enabled = false return end if name == "message_log_close_button" then element.parent.visible = not element.parent.visible return end if name == "toggle_message_log_button" then local gui = player.gui.center if gui.message_log_frame then gui.message_log_frame.visible = not gui.message_log_frame.visible end element.sprite = "message_log_icon" return end if name == "toggle_objective_button" then local gui = player.gui.goal if gui then gui.visible = not gui.visible end return end if story_gui_click then story_gui_click(event) end end function set_continue_button_style(func) for k, player in pairs (game.players) do local button = find_gui_recursive(player.gui, "story_continue_button") if button then func(button) end end end --Should only be used in mini-tutorials for convenience function player(i) if not i then i = 1 end return game.players[i] end --Use carefully, it might not be what you want function surface(i) if not i then i = 1 end return game.surfaces[i] end function deconstruct_on_tick(entities, seconds) local tick = game.tick if entities then seconds = seconds or 1 local duration = seconds*60 --Shuffle the table local rand = math.random local size = #entities for k, entity in pairs (entities) do local index = rand(size) entities[k], entities[index] = entities[index], entities[k] end local new_table = {} for k = 1, duration do new_table[tick+k] = {} end local insert = table.insert for k, entity in pairs (entities) do local tick_to_deconstruct = 1 + tick + (k % duration) insert(new_table[tick_to_deconstruct], entity) end storage.entities_to_deconstruct_on_tick = new_table storage.entities_to_deconstruct_on_tick.end_tick = tick + duration return end if tick > storage.entities_to_deconstruct_on_tick.end_tick then storage.entities_to_deconstruct_on_tick = nil return true end local entities = storage.entities_to_deconstruct_on_tick[tick] if entities then for k, entity in pairs (entities) do if entity.valid then --surface().create_entity{name = "explosion", position = entity.position} entity.destroy() end end storage.entities_to_deconstruct_on_tick[tick] = nil end end function recreate_entities_on_tick(entities, param, seconds) local tick = game.tick if entities then local duration if seconds then duration = seconds*60 else duration = #entities end local new_table = {} for k = 1, duration do new_table[k+tick] = {} end local insert = table.insert for k, entity in pairs (entities) do local tick_to_build = 1 + tick + ((k-1) % duration) insert(new_table[tick_to_build], entity) end new_table.param = param new_table.end_tick = tick + duration storage.entities_to_build_on_tick = new_table return end if tick > storage.entities_to_build_on_tick.end_tick then storage.entities_to_build_on_tick = nil return true end local entities = storage.entities_to_build_on_tick[tick] if entities then recreate_entities(entities, storage.entities_to_build_on_tick.param) storage.entities_to_build_on_tick[tick] = nil end end function flying_congrats(position) if not position then position = player().position end if not position.x then position.x = position[1] end if not position.y then position.y = position[2] end player().create_local_flying_text{text = {"tutorial-gui.objective-complete"}, position = {position.x, position.y - 1.5}, color = {r = 0.1, g = 1, b = 0.1}, time_to_live = 150} end