mirror of
https://github.com/Refactorio/RedMew.git
synced 2024-12-14 10:13:13 +02:00
6aed6d6317
Add map loader
490 lines
18 KiB
Lua
490 lines
18 KiB
Lua
local Game = require 'utils.game'
|
|
local Event = require 'utils.event'
|
|
local Task = require 'utils.task'
|
|
local Token = require 'utils.token'
|
|
local Popup = require 'features.gui.popup'
|
|
local Global = require 'utils.global'
|
|
local Command = require 'utils.command'
|
|
local RS = require 'map_gen.shared.redmew_surface'
|
|
|
|
local random = math.random
|
|
local insert = table.insert
|
|
|
|
if not global.map.terraforming then
|
|
global.map.terraforming = {}
|
|
end
|
|
|
|
--[[
|
|
Some inspiration and bits of code taken from Nightfall by Yehn and dangOreus by Mylon.
|
|
Both under their respective MIT licenses.
|
|
|
|
This softmod originally used the Starcraft concept of "creep" as a mechanic. This was later changed
|
|
to a terraforming context, but most of the code and documentation will keep the creep terminology.
|
|
Players can only build on certain creep tiles (in particular, the defined `creep_expansion_tile`s).
|
|
Pollution will naturally expand the creep. The creep will also naturally regress if pollution
|
|
is not sustained, affecting player structures in the process.
|
|
|
|
This module does not create a map/safe start area, so you will not have a guaranteed safe placed
|
|
to build, and any creep_expansion_tiles will be overtaken by creep_retraction_tiles as there is
|
|
no initial pollution cloud. You can use creep-exempt tiles for a safe starting area.
|
|
|
|
Would love to make the expansion less "blocky"/constrained to chunk borders.
|
|
]]
|
|
|
|
-- lets you know when creep is spreading/contracting (it is very verbose/annoying)
|
|
global.DEBUG_SPREAD = false
|
|
-- lets you know when your bonuses/penalties change based on being on creep
|
|
global.DEBUG_ON_CREEP = false
|
|
|
|
--how many chunks to process in a tick
|
|
local processchunk = 5
|
|
-- the amount of damage taken when too far from creep
|
|
local off_creep_damage = 15
|
|
-- how often to check for players' positions
|
|
local player_pos_check_time = 180 -- 3 secs
|
|
-- the amount of extra damage to deal if a player has shields (because they will regen health)
|
|
local regen_factor = 0.194 * player_pos_check_time
|
|
-- how often to recap the deaths from lack of creep (in ticks)
|
|
local death_recap_timer = 1200 -- 20 secs
|
|
-- force that is restricted to the creep
|
|
local creep_force = 'player'
|
|
-- the threshold above which creep expands
|
|
local pollution_upper_threshold = 200
|
|
-- the threshold below which creep retracts
|
|
local pollution_lower_threshold = 100
|
|
-- the number of tiles that change to/from creep at once
|
|
local random_factor = 0.1
|
|
|
|
-- the message to pop up when players build on the wrong tiles
|
|
local popup_message = 'You may only build on terraformed land! Terraform by spreading pollution.'
|
|
-- the message attached to the number of entities destroyed from not being on creep tiles
|
|
local death_recap_message = ' buildings have died to the toxic atmosphere recently.'
|
|
-- message printed to player when taking damage from being away from the creep
|
|
local player_damage_message = 'You are taking damage from the toxic atmosphere.'
|
|
-- message printed to player when they are ejected from a vehicle from being away from the creep
|
|
local vehicle_ejecte_message = 'The toxic atmosphere wreaks havoc on your controls and you find yourself ejected from the vehicle.'
|
|
-- message printed to game when a player dies to the atmosphere
|
|
local player_death_message = 'The toxic atmosphere has claimed a victim.'
|
|
-- message printed to the game when a bot is destroyed placing a tile
|
|
local dead_robot_recap_message = ' robots died trying to place tiles, it seemed as though the ground swelled up swallowed them whole.'
|
|
-- message printed to the game when a player tries placing a tile
|
|
local player_built_tile_message = 'The ground rejects the artificial tiles you have tried to place'
|
|
|
|
-- boosts while on creep
|
|
local boosts = {
|
|
['character_running_speed_modifier'] = 1.1,
|
|
['character_mining_speed_modifier'] = 1.3,
|
|
['character_crafting_speed_modifier'] = 1,
|
|
['character_health_bonus'] = 200,
|
|
}
|
|
|
|
-- which tiles to use for creep expansion
|
|
local creep_expansion_tiles = global.map.terraforming.creep_expansion_tiles or {
|
|
'grass-1',
|
|
'grass-2',
|
|
}
|
|
-- which tiles to use when creep retracts
|
|
local creep_retraction_tiles = global.map.terraforming.creep_retraction_tiles or {
|
|
'dirt-1',
|
|
'dirt-2',
|
|
'dry-dirt',
|
|
'sand-1',
|
|
'sand-2',
|
|
'sand-3',
|
|
}
|
|
-- which tiles players can build on/count as creep
|
|
local creep_tiles = global.map.terraforming.creep_tiles or {
|
|
'grass-1',
|
|
'grass-2',
|
|
'concrete',
|
|
'hazard-concrete-left',
|
|
'hazard-concrete-right',
|
|
'refined-concrete',
|
|
'refined-hazard-concrete-left',
|
|
'refined-hazard-concrete-right',
|
|
'stone-path',
|
|
}
|
|
-- tiles which creep can expand into
|
|
local non_creep_tiles = global.map.terraforming.non_creep_tiles or {
|
|
'dirt-1',
|
|
'dirt-2',
|
|
'dirt-3',
|
|
'dirt-4',
|
|
'dirt-5',
|
|
'dirt-6',
|
|
'dirt-7',
|
|
'dry-dirt',
|
|
'grass-3',
|
|
'grass-4',
|
|
'red-desert-0',
|
|
'red-desert-1',
|
|
'red-desert-2',
|
|
'red-desert-3',
|
|
'sand-1',
|
|
'sand-2',
|
|
'sand-3',
|
|
}
|
|
-- Tiles which are completely exempt from the creep mechanic, currently kept in comment as a reference:
|
|
-- 'deepwater', 'deepwater-green', 'out-of-map', 'water', 'water-green', 'lab-dark-1', 'lab-dark-2', 'lab-white', 'tutorial-grid'
|
|
local decoratives = {
|
|
'brown-asterisk',
|
|
'brown-carpet-grass',
|
|
'brown-fluff',
|
|
'brown-fluff-dry',
|
|
'brown-hairy-grass',
|
|
'garballo',
|
|
'garballo-mini-dry',
|
|
'green-asterisk',
|
|
'green-bush-mini',
|
|
'green-carpet-grass',
|
|
'green-hairy-grass',
|
|
'green-pita',
|
|
'green-pita-mini',
|
|
'green-small-grass',
|
|
'red-asterisk'
|
|
}
|
|
|
|
-- the 5 states a chunk can be in
|
|
local NOT_CREEP = 1 -- Chunk is 0% creep tiles and unpolluted
|
|
local FULL_CREEP = 2 -- Chunk is 100% creep tile and polluted
|
|
local CREEP_RETRACTION = 3 -- Chunk has >0% creep tiles but is unpolluted
|
|
local CREEP_EXPANDING = 4 -- Chunk has <100% creep tiles but is polluted
|
|
local CREEP_UNKNOWN = 5 -- a special case for newly-generated chunks where we need to check their state
|
|
|
|
-- Register our globals
|
|
local chunklist = {}
|
|
local popup_timeout = {}
|
|
local death_count = {0}
|
|
local dead_robot_count = {0}
|
|
local c_index = {1}
|
|
|
|
Global.register(
|
|
{
|
|
chunklist = chunklist,
|
|
death_count = death_count,
|
|
c_index = c_index,
|
|
dead_robot_count = dead_robot_count,
|
|
popup_timeout = popup_timeout,
|
|
},
|
|
function(tbl)
|
|
chunklist = tbl.chunklist
|
|
death_count = tbl.death_count
|
|
c_index = tbl.c_index
|
|
dead_robot_count = tbl.dead_robot_count
|
|
popup_timeout = tbl.popup_timeout
|
|
end
|
|
)
|
|
|
|
--- Converts tiles
|
|
-- @param tile_table table of tiles to convert
|
|
-- @param tiles table of potential tiles to convert to
|
|
local function convert_tiles(tile_table, tiles)
|
|
local set_tiles = RS.get_surface().set_tiles
|
|
local tile_set = {}
|
|
local target_tile = tile_table[random(1, #tile_table)]
|
|
-- convert the LuaTiles table into a new one we can edit
|
|
for _, tiledata in pairs(tiles) do
|
|
if random() < random_factor then
|
|
tile_set[#tile_set + 1] = {name = target_tile, position = tiledata.position}
|
|
end
|
|
end
|
|
-- change the tiles to the target_tile
|
|
set_tiles(tile_set)
|
|
end
|
|
|
|
local on_popup_timeout_complete =
|
|
Token.register(
|
|
function(name)
|
|
popup_timeout[name] = nil
|
|
end
|
|
)
|
|
|
|
--- Kills buildings that are not on creep tiles
|
|
-- @param entity LuaEntity to kill
|
|
-- @param event or false - whether the entity is coming from a build event
|
|
local function kill_invalid_builds(event)
|
|
local entity = event.created_entity
|
|
if not (entity and entity.valid) then
|
|
return
|
|
end
|
|
-- don't kill players
|
|
if entity.type == 'player' then
|
|
return
|
|
end
|
|
-- don't kill vehicles
|
|
if entity.type == 'car' or entity.type == 'tank' or not entity.health then
|
|
return
|
|
end
|
|
-- Some entities have no bounding box area. Not sure which.
|
|
if entity.bounding_box.left_top.x == entity.bounding_box.right_bottom.x or entity.bounding_box.left_top.y == entity.bounding_box.right_bottom.y then
|
|
return
|
|
end
|
|
-- don't kill trains
|
|
if entity.type == 'locomotive' or entity.type == 'fluid-wagon' or entity.type == 'cargo-wagon' or entity.type == 'artillery-wagon ' then
|
|
return
|
|
end
|
|
|
|
local last_user = entity.last_user
|
|
local ceil = math.ceil
|
|
local floor = math.floor
|
|
|
|
-- expand the bounding box to enclose full tiles to be scanned (if your area is less than the full size of the tile, the tile is not included)
|
|
local bounding_box = {
|
|
{floor(entity.bounding_box.left_top.x), floor(entity.bounding_box.left_top.y)},
|
|
{ceil(entity.bounding_box.right_bottom.x), ceil(entity.bounding_box.right_bottom.y)}
|
|
}
|
|
local tiles = entity.surface.count_tiles_filtered {name = non_creep_tiles, area = bounding_box, limit = 1}
|
|
if tiles > 0 then
|
|
--Need to turn off ghosts left by dead buildings so construction bots won't keep placing buildings and having them blow up.
|
|
local force = entity.force
|
|
local ttl = force.ghost_time_to_live
|
|
entity.force.ghost_time_to_live = 0
|
|
entity.die()
|
|
force.ghost_time_to_live = ttl
|
|
death_count[1] = death_count[1] + 1
|
|
-- checking for event.tick is a cheap way to see if it's an actual event or if the event data came from check_chunk_for_entities
|
|
if event.tick and last_user and last_user.connected and not popup_timeout[last_user.name] then
|
|
Popup.player(last_user, popup_message)
|
|
popup_timeout[last_user.name] = true
|
|
Task.set_timeout(60, on_popup_timeout_complete, last_user.name)
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Scans the provided chunk for entities on force _creep_force_.
|
|
--@param chunk table with position and status of a map chunk
|
|
local function check_chunk_for_entities(chunk)
|
|
local find_entities_filtered = RS.get_surface().find_entities_filtered
|
|
local entities_found
|
|
entities_found =
|
|
find_entities_filtered {
|
|
area = {{chunk.x - 16, chunk.y - 16}, {chunk.x + 16, chunk.y + 16}},
|
|
force = creep_force
|
|
}
|
|
for _, entity in pairs(entities_found) do
|
|
kill_invalid_builds({['created_entity'] = entity})
|
|
end
|
|
end
|
|
|
|
--- Changes the state and tiles of chunks when they meet the creep expansion/retraction criteria
|
|
--@param state number representing whether we want to expand or contract the chunk (expand = 1, retract = 2)
|
|
--@param i number of the chunk's key in the chunklist table
|
|
local function change_creep_state(state, i)
|
|
local find_tiles_filtered = RS.get_surface().find_tiles_filtered
|
|
local tiles_to_set = {}
|
|
local debug_message
|
|
local chunk_end_state
|
|
local chunk_transition_state
|
|
local tiles_to_find
|
|
|
|
-- states: expand = 1, retract = 2
|
|
if state == 1 then
|
|
tiles_to_find = non_creep_tiles
|
|
tiles_to_set = creep_expansion_tiles
|
|
debug_message = 'Creep expanding'
|
|
chunk_end_state = FULL_CREEP
|
|
chunk_transition_state = CREEP_EXPANDING
|
|
elseif state == 2 then
|
|
tiles_to_find = creep_tiles
|
|
tiles_to_set = creep_retraction_tiles
|
|
debug_message = 'Creep retracting'
|
|
chunk_end_state = NOT_CREEP
|
|
chunk_transition_state = CREEP_RETRACTION
|
|
end
|
|
|
|
chunklist[i].is_creep = chunk_transition_state
|
|
local chunkcoord = chunklist[i]
|
|
local tiles =
|
|
find_tiles_filtered(
|
|
{
|
|
area = {{chunkcoord.x - 16, chunkcoord.y - 16}, {chunkcoord.x + 16, chunkcoord.y + 16}},
|
|
name = tiles_to_find
|
|
}
|
|
)
|
|
if (#tiles > 0) then
|
|
convert_tiles(tiles_to_set, tiles)
|
|
if global.DEBUG_SPREAD then
|
|
game.print(debug_message)
|
|
end
|
|
else
|
|
-- if there are 0 tiles to convert, they're either fully creep or fully non-creep
|
|
chunklist[i].is_creep = chunk_end_state
|
|
-- if a chunk has lost all creep, do a final check to see if there are any buildings to kill and regen the decoratives
|
|
if state == 2 then
|
|
check_chunk_for_entities(chunklist[i])
|
|
RS.get_surface().regenerate_decorative(decoratives, {{chunklist[i].x, chunklist[i].y}})
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Every tick scan _processchunk_ number of chunks for their pollution state and if needed, change their state
|
|
local function on_tick()
|
|
local get_pollution = RS.get_surface().get_pollution
|
|
local maxindex = #chunklist
|
|
for i = c_index[1], c_index[1] + processchunk, 1 do
|
|
if i > maxindex then
|
|
-- we've iterated through all chunks
|
|
c_index[1] = 1
|
|
break
|
|
end
|
|
if get_pollution(chunklist[i]) > pollution_upper_threshold and chunklist[i].is_creep ~= FULL_CREEP then
|
|
change_creep_state(1, i) -- expand = 1, retract = 2
|
|
elseif get_pollution(chunklist[i]) < pollution_lower_threshold and chunklist[i].is_creep ~= NOT_CREEP then
|
|
change_creep_state(2, i) -- expand = 1, retract = 2
|
|
end
|
|
if chunklist[i].is_creep == CREEP_RETRACTION then
|
|
-- if a chunk's creep is retracting, we need to check if there are entities to kill
|
|
check_chunk_for_entities(chunklist[i])
|
|
end
|
|
end
|
|
c_index[1] = c_index[1] + processchunk
|
|
end
|
|
|
|
--- Takes newly generated chunks and places them inside the chunklist table
|
|
local function on_chunk_generated(event)
|
|
if event.surface == RS.get_surface() then
|
|
local chunk = {}
|
|
local coords = event.area.left_top
|
|
chunk.x = coords.x + 16
|
|
chunk.y = coords.y + 16
|
|
chunk.is_creep = CREEP_UNKNOWN
|
|
insert(chunklist, chunk)
|
|
end
|
|
end
|
|
|
|
--- Prints the number of deaths from buildings outside of creep. Resets every _death_recap_timer_ ticks.
|
|
local function print_death_recap()
|
|
if death_count[1] > 1 then
|
|
game.print(death_count[1] .. death_recap_message)
|
|
death_count[1] = 0
|
|
end
|
|
if dead_robot_count[1] > 1 then
|
|
game.print(dead_robot_count[1] .. dead_robot_recap_message)
|
|
dead_robot_count[1] = 0
|
|
end
|
|
end
|
|
|
|
--- Apply penalties for being away from creep
|
|
local function apply_penalties(p, c)
|
|
for boost in pairs(boosts) do
|
|
p[boost] = 0
|
|
end
|
|
c.disable_flashlight()
|
|
end
|
|
|
|
--- Gives movement speed buffs when on creep, slows when only near creep, damages when far from creep.
|
|
local function apply_creep_effects_on_players()
|
|
local radius = 10 --distance to check around player for creep (nb. not actually a radius)
|
|
for _, p in pairs(game.connected_players) do
|
|
local c = p.character
|
|
if c then
|
|
-- count all non_creep_tiles around the player
|
|
local count = p.surface.count_tiles_filtered {name = non_creep_tiles, area = {{p.position.x - radius, p.position.y - radius}, {p.position.x + radius, p.position.y + radius}}}
|
|
if count == (radius * 2) ^ 2 then
|
|
-- kick player from vehicle
|
|
if p.vehicle then
|
|
p.driving = false
|
|
p.print(vehicle_ejecte_message)
|
|
end
|
|
-- calculate damage based on whether player has shields and is not in combat and check to see if we would deal lethal damage
|
|
-- (shields prevent us putting the character into combat, so we need to compensate for health regen)
|
|
local message = player_damage_message
|
|
local damage
|
|
if c.grid and c.grid.shield and not c.in_combat then
|
|
damage = off_creep_damage + regen_factor
|
|
message = message .. ' Your shields do nothing to help.'
|
|
else
|
|
damage = off_creep_damage
|
|
end
|
|
if (damage + 10) >= c.health then -- add 10 for the acid projectile damage
|
|
c.die('enemy')
|
|
game.print(player_death_message)
|
|
return
|
|
end
|
|
-- create acid splash and deal damage
|
|
p.surface.create_entity {name = 'acid-projectile-purple', target = p.character, position = p.character.position, speed = 10}
|
|
c.health = c.health - damage
|
|
p.print(message)
|
|
-- apply penalties for being away from creep
|
|
apply_penalties(p, c)
|
|
if global.DEBUG_ON_CREEP then
|
|
game.print('Far from creep and taking damage')
|
|
end
|
|
elseif count > (radius * 2) ^ 2 * 0.8 then
|
|
-- apply penalties for being away from creep
|
|
apply_penalties(p, c)
|
|
if global.DEBUG_ON_CREEP then
|
|
game.print('Near but not on creep')
|
|
end
|
|
else
|
|
-- apply boosts for being on or near creep
|
|
for boost, boost_value in pairs(boosts) do
|
|
p[boost] = boost_value
|
|
end
|
|
c.enable_flashlight()
|
|
if global.DEBUG_ON_CREEP then
|
|
game.print('On creep and getting full benefits')
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Revert built tiles
|
|
local function check_on_tile_built(event)
|
|
local surface
|
|
if event.robot then
|
|
surface = event.robot.surface
|
|
event.robot.die('enemy')
|
|
dead_robot_count[1] = dead_robot_count[1] + 1
|
|
else
|
|
local player = Game.get_player_by_index(event.player_index)
|
|
surface = player.surface
|
|
player.print(player_built_tile_message)
|
|
end
|
|
global.temp = event.tiles
|
|
local tile_set = {}
|
|
insert = table.insert
|
|
for k, v in pairs(event.tiles) do
|
|
tile_set[#tile_set + 1] = {name = v.old_tile.name, position = v.position}
|
|
end
|
|
surface.set_tiles(tile_set)
|
|
end
|
|
|
|
Event.add(defines.events.on_tick, on_tick)
|
|
Event.add(defines.events.on_chunk_generated, on_chunk_generated)
|
|
Event.add(defines.events.on_built_entity, kill_invalid_builds)
|
|
Event.add(defines.events.on_robot_built_entity, kill_invalid_builds)
|
|
Event.add(defines.events.on_player_built_tile, check_on_tile_built)
|
|
Event.add(defines.events.on_robot_built_tile, check_on_tile_built)
|
|
Event.on_nth_tick(death_recap_timer, print_death_recap)
|
|
Event.on_nth_tick(player_pos_check_time, apply_creep_effects_on_players)
|
|
|
|
--- Debug commands which will generate or clear pollution
|
|
Command.add(
|
|
'cloud',
|
|
{
|
|
description = 'Create a lot of pollution',
|
|
debug_only = true,
|
|
cheat_only = true
|
|
},
|
|
function()
|
|
if game.player then
|
|
game.player.surface.pollute(game.player.position, 10000)
|
|
end
|
|
end
|
|
)
|
|
Command.add(
|
|
'clean',
|
|
{
|
|
description = 'Eliminate all pollution on the surface',
|
|
debug_only = true,
|
|
cheat_only = true
|
|
},
|
|
function()
|
|
if game.player then
|
|
game.player.surface.clear_pollution()
|
|
end
|
|
end
|
|
)
|