--[[-- info Provides the ability to collapse caves when digging. ]] -- dependencies local Event = require 'utils.event' local Template = require 'map_gen.maps.diggy.template' local ScoreTracker = require 'utils.score_tracker' local Debug = require 'map_gen.maps.diggy.debug' local Task = require 'utils.task' local Token = require 'utils.token' local Global = require 'utils.global' local CreateParticles = require 'features.create_particles' local RS = require 'map_gen.shared.redmew_surface' local Popup = require 'features.gui.popup' local table = require 'utils.table' local random = math.random local floor = math.floor local pairs = pairs local pcall = pcall local is_diggy_rock = Template.is_diggy_rock local template_insert = Template.insert local raise_event = script.raise_event local set_timeout = Task.set_timeout local set_timeout_in_ticks = Task.set_timeout_in_ticks local ceiling_crumble = CreateParticles.ceiling_crumble local clear_table = table.clear_table local collapse_rocks = Template.diggy_rocks local collapse_rocks_size = #collapse_rocks local cave_collapses_name = 'cave-collapses' -- this local DiggyCaveCollapse = {} local config local n = 9 local radius = 0 local radius_sq = 0 local center_radius_sq = 0 local disc_radius_sq = 0 local center_weight local disc_weight local ring_weight local disc_blur_sum = 0 local center_value = 0 local disc_value = 0 local ring_value = 0 local enable_stress_grid = 0 local stress_map_add local mask_disc_blur local mask_init local stress_map_check_stress_in_threshold local support_beam_entities local on_surface_created local stress_threshold_causing_collapse = 3.57 local near_stress_threshold_causing_collapse = 3.3 -- just above the threshold of a normal 4 pillar grid local show_deconstruction_alert_message = {} local stress_map_storage = {} local new_tile_map = {} local collapse_positions_storage = {} Global.register( { new_tile_map = new_tile_map, stress_map_storage = stress_map_storage, deconstruction_alert_message_shown = show_deconstruction_alert_message, collapse_positions_storage = collapse_positions_storage }, function(tbl) new_tile_map = tbl.new_tile_map stress_map_storage = tbl.stress_map_storage show_deconstruction_alert_message = tbl.deconstruction_alert_message_shown collapse_positions_storage = tbl.collapse_positions_storage end ) local defaultValue = 0 local collapse_alert = {type = 'item', name = 'stone'} DiggyCaveCollapse.events = { --[[-- When stress at certain position is above the collapse threshold - position LuaPosition - surface LuaSurface - player_index Number (index of player that caused the collapse) ]] on_collapse_triggered = Event.generate_event_name('on_collapse_triggered'), --[[-- After a collapse - position LuaPosition - surface LuaSurface - player_index Number (index of player that caused the collapse) ]] on_collapse = Event.generate_event_name('on_collapse') } local function create_collapse_template(positions, surface) local entities = {} local entity_count = 0 local find_entities_filtered = surface.find_entities_filtered for _, position in pairs(positions) do local x = position.x local y = position.y local do_insert = true for _, entity in pairs(find_entities_filtered({area = {position, {x + 1, y + 1}}})) do pcall( function() local strength = support_beam_entities[entity.name] if strength then do_insert = false else if entity.name ~= 'tank' then entity.die() else entity.health = entity.health - 100 if entity.health == 0 then entity.die() end end end end ) end if do_insert then entity_count = entity_count + 1 entities[entity_count] = {position = position, name = collapse_rocks[random(collapse_rocks_size)]} end end return entities end local function create_collapse_alert(surface, position) local target = surface.create_entity({position = position, name = 'rock-big'}) for _, player in pairs(game.connected_players) do player.add_custom_alert(target, collapse_alert, {'diggy.cave_collapse'}, true) end target.destroy() end local function collapse(args) local position = args.position local surface = args.surface local positions = {} local count = 0 local strength = config.collapse_threshold_total_strength mask_disc_blur( position.x, position.y, strength, function(x, y, value) stress_map_check_stress_in_threshold( surface, x, y, value, function(_, c_x, c_y) count = count + 1 positions[count] = {x = c_x, y = c_y} end ) end ) if #positions == 0 then return end create_collapse_alert(surface, position) template_insert(surface, {}, create_collapse_template(positions, surface)) raise_event(DiggyCaveCollapse.events.on_collapse, args) ScoreTracker.change_for_global(cave_collapses_name, 1) end local on_collapse_timeout_finished = Token.register(collapse) local on_near_threshold = Token.register( function(params) ceiling_crumble(params.surface, params.position) end ) local function spawn_collapse_text(surface, position) local color = { r = 1, g = random(1, 100) * 0.01, b = 0 } surface.create_entity({ name = 'tutorial-flying-text', color = color, text = config.cracking_sounds[random(#config.cracking_sounds)], position = position, }) end local function on_collapse_triggered(event) local surface = event.surface local position = event.position local x = position.x local y = position.y local x_t = new_tile_map[x] if x_t and x_t[y] then template_insert(surface, {}, {{position = position, name = 'rock-big'}}) return end spawn_collapse_text(surface, position) set_timeout(config.collapse_delay, on_collapse_timeout_finished, event) end local function on_built_tile(surface, new_tile, tiles) local new_tile_strength = support_beam_entities[new_tile.name] for _, tile in pairs(tiles) do if new_tile_strength then stress_map_add(surface, tile.position, -1 * new_tile_strength, true) end local old_tile_strength = support_beam_entities[tile.old_tile.name] if (old_tile_strength) then stress_map_add(surface, tile.position, old_tile_strength, true) end end end --It is impossible to track which player marked the tile for deconstruction local function on_robot_mined_tile(event) local surface for _, tile in pairs(event.tiles) do local strength = support_beam_entities[tile.old_tile.name] if strength then surface = surface or event.robot.surface stress_map_add(surface, tile.position, strength, true) end end end local function on_player_mined_tile(event) local surface = game.surfaces[event.surface_index] for _, tile in pairs(event.tiles) do local strength = support_beam_entities[tile.old_tile.name] if strength then stress_map_add(surface, tile.position, strength, true, event.player_index) end end end local function on_mined_entity(event) local entity = event.entity local name = entity.name local strength = support_beam_entities[name] if strength then local player_index if not is_diggy_rock(name) then player_index = event.player_index end stress_map_add(entity.surface, entity.position, strength, false, player_index) end end local function on_entity_died(event) local entity = event.entity local name = entity.name local strength = support_beam_entities[name] if strength then local player_index if not is_diggy_rock(name) then local cause = event.cause player_index = cause and cause.type == 'character' and cause.player and cause.player.index or nil end stress_map_add(entity.surface, entity.position, strength, false, player_index) end end local function script_raised_destroy(event) local cause = event.cause if cause and not (cause == "room_clearing" or cause == "die_faster" or cause == "alien_emerges") then return end local entity = event.entity local name = entity.name local strength = support_beam_entities[name] if strength then stress_map_add(entity.surface, entity.position, strength, false) end end local function on_built_entity(event) local entity = event.created_entity local strength = support_beam_entities[entity.name] if strength then stress_map_add(entity.surface, entity.position, -1 * strength) end end local function on_placed_entity(event) local strength = support_beam_entities[event.entity.name] if strength then stress_map_add(event.entity.surface, event.entity.position, -1 * strength) end end local on_new_tile_timeout_finished = Token.register( function(args) local x_t = new_tile_map[args.x] if x_t then x_t[args.y] = nil --reset new tile status. This tile can cause a chain collapse now end end ) local function on_void_removed(event) local strength = support_beam_entities['out-of-map'] local position = event.position if strength then stress_map_add(event.surface, position, strength) end local x = position.x local y = position.y --To avoid room collapse: local x_t = new_tile_map[x] if x_t then x_t[y] = true else x_t = { [y] = true } new_tile_map[x] = x_t end set_timeout(3, on_new_tile_timeout_finished, {x = x, y = y}) end --[[-- Registers all event handlers.] @param global_config Table {@see Diggy.Config}. ]] --Special thanks to justarandomgeek from the main factorio discord guild for helping to teach orange how image classes work. function DiggyCaveCollapse.register(cfg) ScoreTracker.register(cave_collapses_name, {'diggy.score_cave_collapses'}, '[img=entity.small-remnants]') local global_to_show = global.config.score.global_to_show global_to_show[#global_to_show + 1] = cave_collapses_name config = cfg support_beam_entities = config.support_beam_entities if support_beam_entities['stone-path'] then support_beam_entities['stone-brick'] = support_beam_entities['stone-path'] else support_beam_entities['stone-brick'] = nil end if support_beam_entities['hazard-concrete'] then support_beam_entities['hazard-concrete-left'] = support_beam_entities['hazard-concrete'] support_beam_entities['hazard-concrete-right'] = support_beam_entities['hazard-concrete'] else support_beam_entities['hazard-concrete-left'] = nil support_beam_entities['hazard-concrete-right'] = nil end if support_beam_entities['refined-hazard-concrete'] then support_beam_entities['refined-hazard-concrete-left'] = support_beam_entities['refined-hazard-concrete'] support_beam_entities['refined-hazard-concrete-right'] = support_beam_entities['refined-hazard-concrete'] else support_beam_entities['refined-hazard-concrete-left'] = nil support_beam_entities['refined-hazard-concrete-right'] = nil end Event.add(DiggyCaveCollapse.events.on_collapse_triggered, on_collapse_triggered) Event.add(defines.events.on_robot_built_entity, on_built_entity) Event.add( defines.events.on_robot_built_tile, function(event) on_built_tile(event.robot.surface, event.item, event.tiles) end ) Event.add( defines.events.on_player_built_tile, function(event) on_built_tile(game.surfaces[event.surface_index], event.tile, event.tiles) end ) Event.add(defines.events.on_robot_mined_tile, on_robot_mined_tile) Event.add(defines.events.on_player_mined_tile, on_player_mined_tile) Event.add(defines.events.on_built_entity, on_built_entity) Event.add(Template.events.on_placed_entity, on_placed_entity) Event.add(defines.events.on_entity_died, on_entity_died) Event.add(defines.events.script_raised_destroy, script_raised_destroy) Event.add(defines.events.on_player_mined_entity, on_mined_entity) Event.add(Template.events.on_void_removed, on_void_removed) Event.add(defines.events.on_surface_created, on_surface_created) Event.add( defines.events.on_marked_for_deconstruction, function(event) local entity = event.entity local name = entity.name if is_diggy_rock(name) then return end if name == 'deconstructible-tile-proxy' or nil ~= support_beam_entities[name] then entity.cancel_deconstruction(game.get_player(event.player_index).force) end end ) Event.add( defines.events.on_player_created, function(event) show_deconstruction_alert_message[event.player_index] = true end ) Event.add( defines.events.on_pre_player_mined_item, function(event) local player_index = event.player_index if not show_deconstruction_alert_message[player_index] then return end if (nil ~= support_beam_entities[event.entity.name]) then Popup.player( game.get_player(player_index), {'diggy.cave_collapse_warning'} ) show_deconstruction_alert_message[player_index] = nil end end ) enable_stress_grid = config.enable_stress_grid on_surface_created({surface_index = 1}) mask_init(config) if (config.enable_mask_debug) then local surface = RS.get_surface() mask_disc_blur( 0, 0, 10, function(x, y, fraction) Debug.print_grid_value(fraction, surface, {x = x, y = y}) end ) end end -- --STRESS MAP -- --[[-- Adds a fraction to a given location on the stress_map. Returns the new fraction value of that position. @param stress_map Table of {x,y} @param position Table with x and y @param number fraction @return number sum of old fraction + new fraction ]] ---Adds a fraction to a given location on the stress_map. Returns the new fraction value of that position. ---@param stress_map table ---@param x number ---@param y number ---@param fraction number ---@param player_index number ---@param surface LuaSurface local function add_fraction(stress_map, x, y, fraction, player_index, surface) x = 2 * floor(x * 0.5) y = 2 * floor(y * 0.5) local x_t = stress_map[x] if not x_t then x_t = {} stress_map[x] = x_t end local value = x_t[y] if not value then value = defaultValue end value = value + fraction x_t[y] = value if fraction > 0 then if value > stress_threshold_causing_collapse then raise_event( DiggyCaveCollapse.events.on_collapse_triggered, { surface = surface, position = {x = x, y = y}, player_index = player_index } ) elseif value > near_stress_threshold_causing_collapse then set_timeout_in_ticks(2, on_near_threshold, {surface = surface, position = {x = x, y = y}}) end end if enable_stress_grid then Debug.print_colored_grid_value(value, surface, {x = x, y = y}, 0.5, false, value / stress_threshold_causing_collapse, {r = 0, g = 1, b = 0}, {r = 1, g = -1, b = 0}, {r = 0, g = 1, b = 0}, {r = 1, g = 1, b = 1}) end return value end on_surface_created = function(event) local index = event.surface_index if stress_map_storage[index] then clear_table(stress_map_storage[index]) else stress_map_storage[index] = {} end local map = stress_map_storage[index] map['surface_index'] = index map[1] = {index = 1} map[2] = {index = 2} map[3] = {index = 3} map[4] = {index = 4} end ---Checks whether a tile's pressure is within a given threshold and calls the handler if not. ---@param surface LuaSurface ---@param x number ---@param y number ---@param threshold number ---@param callback function stress_map_check_stress_in_threshold = function(surface, x, y, threshold, callback) local stress_map = stress_map_storage[surface.index] local value = add_fraction(stress_map, x, y, 0, nil, surface) if (value >= stress_threshold_causing_collapse - threshold) then callback(surface, x, y) end end stress_map_add = function(surface, position, factor, no_blur, player_index) local x_start = floor(position.x) local y_start = floor(position.y) local stress_map = stress_map_storage[surface.index] if not stress_map then return end if no_blur then add_fraction(stress_map, x_start, y_start, factor, player_index, surface) return end for x = -radius, radius do for y = -radius, radius do local value = 0 local distance_sq = x * x + y * y if distance_sq <= center_radius_sq then value = center_value elseif distance_sq <= disc_radius_sq then value = disc_value elseif distance_sq <= radius_sq then value = ring_value end if value > 0.001 or value < -0.001 then add_fraction(stress_map, x + x_start, y + y_start, value * factor, player_index, surface) end end end end DiggyCaveCollapse.stress_map_add = stress_map_add -- -- MASK -- mask_init = function(config) -- luacheck: ignore 431 (intentional upvalue shadow) n = config.mask_size local ring_weights = config.mask_relative_ring_weights ring_weight = ring_weights[1] disc_weight = ring_weights[2] center_weight = ring_weights[3] radius = floor(n * 0.5) radius_sq = (radius + 0.2) * (radius + 0.2) center_radius_sq = radius_sq / 9 disc_radius_sq = radius_sq * 4 / 9 for x = -radius, radius do for y = -radius, radius do local distance_sq = x * x + y * y if distance_sq <= center_radius_sq then disc_blur_sum = disc_blur_sum + center_weight elseif distance_sq <= disc_radius_sq then disc_blur_sum = disc_blur_sum + disc_weight elseif distance_sq <= radius_sq then disc_blur_sum = disc_blur_sum + ring_weight end end end center_value = center_weight / disc_blur_sum disc_value = disc_weight / disc_blur_sum ring_value = ring_weight / disc_blur_sum end --[[-- Applies a blur Applies the disc in 3 discs: center, (middle) disc and (outer) ring. The relative weights for tiles in a disc are: center: 3/3 disc: 2/3 ring: 1/3 The sum of all values is 1 @param x_start number center point @param y_start number center point @param factor the factor to multiply the cell value with (value = cell_value * factor) @param callback function to execute on each tile within the mask callback(x, y, value) ]] mask_disc_blur = function(x_start, y_start, factor, callback) x_start = floor(x_start) y_start = floor(y_start) for x = -radius, radius do for y = -radius, radius do local value = 0 local distance_sq = x * x + y * y if distance_sq <= center_radius_sq then value = center_value elseif distance_sq <= disc_radius_sq then value = disc_value elseif distance_sq <= radius_sq then value = ring_value end if value > 0.001 or value < -0.001 then callback(x_start + x, y_start + y, value * factor) end end end end function DiggyCaveCollapse.get_extra_map_info() return [[Cave Collapse, it might just collapse! Place stone walls, stone paths and (refined) concrete to reinforce the mine. If you see cracks appear, run!]] end Event.on_init( function() if global.config.redmew_surface.enabled then on_surface_created({surface_index = RS.get_surface().index}) end end ) return DiggyCaveCollapse