1
0
mirror of https://github.com/Oarcinae/FactorioScenarioMultiplayerSpawn.git synced 2025-02-07 13:07:58 +02:00
Oarcinae 4cd397f815 Add basic support for Vulcanus secondary spawns.
Have a workaround for demolishers that tracks them and destroys them if they are too close to the spawn. Can definitely be abused, but good enough for now. Fixed issue with joiners being teleported unexpectedly when rerolling a secondary spawn. Removed some old commented out code in utils. Tweak default danger radius to be smaller based on feedback. Fix fuild resources and sharing entities not moving when scaling spawn size. Added locale for welcome home ground text.
2024-11-19 20:59:25 -05:00

1578 lines
57 KiB
Lua

-- My general purpose utility functions and constants for factorio
-- Also contains some constants
require("lib/oarc_gui_utils")
require("mod-gui")
local util = require("util")
--------------------------------------------------------------------------------
-- Useful constants
--------------------------------------------------------------------------------
CHUNK_SIZE = 32
MAX_FORCES = 64
TICKS_PER_SECOND = 60
TICKS_PER_MINUTE = TICKS_PER_SECOND * 60
TICKS_PER_HOUR = TICKS_PER_MINUTE * 60
MAX_INT32_POS = 2147483647
MAX_INT32_NEG = -2147483648
--------------------------------------------------------------------------------
-- --------------------------------------------------------------------------------
-- -- General Helper Functions
-- --------------------------------------------------------------------------------
-- -- Prints flying text.
-- -- Color is optional
-- function FlyingText(msg, pos, color, surface)
-- if color == nil then
-- surface.create_entity({ name = "flying-text", position = pos, text = msg })
-- else
-- surface.create_entity({ name = "flying-text", position = pos, text = msg, color = color })
-- end
-- end
-- Get a printable GPS string
---@param surface_name string
---@param position MapPosition
---@return string
function GetGPStext(surface_name, position)
return "[gps=" .. position.x .. "," .. position.y .. "," .. surface_name .. "]"
end
-- -- Requires having an on_tick handler.
-- function DisplaySpeechBubble(player, text, timeout_secs)
-- if (storage.oarc_speech_bubbles == nil) then
-- storage.oarc_speech_bubbles = {}
-- end
-- if (player and player.character) then
-- local sp = player.surface.create_entity{name = "compi-speech-bubble",
-- position = player.position,
-- text = text,
-- source = player.character}
-- table.insert(storage.oarc_speech_bubbles, {entity=sp,
-- timeout_tick=game.tick+(timeout_secs*TICKS_PER_SECOND)})
-- end
-- end
---Render some text on the ground. Visible to all players. Forever.
---@param surface LuaSurface
---@param position MapPosition
---@param scale number
---@param text string
---@param color Color
---@param alignment TextAlign?
---@return nil
function RenderPermanentGroundText(surface, position, scale, text, color, alignment)
rendering.draw_text { text = text,
surface = surface,
target = position,
color = color,
scale = scale,
--Allowed fonts: default-dialog-button default-game compilatron-message-font default-large default-large-semibold default-large-bold heading-1 compi
font = "compi",
alignment = alignment,
draw_on_ground = true }
end
---A standardized helper text that fades out over time
---@param text string|LocalisedString
---@param surface LuaSurface
---@param position MapPosition
---@param ttl number
---@param alignment TextAlign?
---@return nil
function TemporaryHelperText(text, surface, position, ttl, alignment)
local render_object = rendering.draw_text { text = text,
surface = surface,
target = position,
color = { 0.7, 0.7, 0.7, 0.7 },
scale = 1,
font = "compi",
time_to_live = ttl,
alignment = alignment,
draw_on_ground = false }
local rid = render_object.id
table.insert(storage.oarc_renders_fadeout, rid)
end
-- -- Every second, check a global table to see if we have any speech bubbles to kill.
-- function TimeoutSpeechBubblesOnTick()
-- if ((game.tick % (TICKS_PER_SECOND)) == 3) then
-- if (storage.oarc_speech_bubbles and (#storage.oarc_speech_bubbles > 0)) then
-- for k,sp in pairs(storage.oarc_speech_bubbles) do
-- if (game.tick > sp.timeout_tick) then
-- if (sp.entity ~= nil) and (sp.entity.valid) then
-- sp.entity.start_fading_out()
-- end
-- table.remove(storage.oarc_speech_bubbles, k)
-- end
-- end
-- end
-- end
-- end
---Every tick, check a global table to see if we have any rendered thing that needs fading out.
---@return nil
function FadeoutRenderOnTick()
if (storage.oarc_renders_fadeout and (#storage.oarc_renders_fadeout > 0)) then
for k, rid in pairs(storage.oarc_renders_fadeout) do
local render_object = rendering.get_object_by_id(rid)
if (render_object and render_object.valid) then
local ttl = render_object.time_to_live
if ((ttl > 0) and (ttl < 200)) then
local color = render_object.color
if (color.a > 0.005) then
render_object.color = { r = color.r, g = color.g, b = color.b, a = color.a - 0.005 }
end
end
else
storage.oarc_renders_fadeout[k] = nil
end
end
end
end
--- Broadcast messages to all connected players
---@param msg LocalisedString
---@return nil
function SendBroadcastMsg(msg)
for name, player in pairs(game.connected_players) do
player.print(msg)
end
end
---Send a message to a player, safely checks if they exist and are online.
---@param playerName string
---@param msg LocalisedString
---@return nil
function SendMsg(playerName, msg)
if ((game.players[playerName] ~= nil) and (game.players[playerName].connected)) then
game.players[playerName].print(msg)
end
end
---Checks if a string starts with another string
---@param string string The string to check
---@param start string The starting string to look for
function StringStartsWith(string, start)
return string:sub(1, #start) == start
end
---Checks if a surface is blacklisted based on the storage.ocfg settings
---@param surface_name string
---@return boolean --true if blacklisted
function IsSurfaceBlacklisted(surface_name)
if (storage.ocfg.surfaces_blacklist ~= nil) then
for _,name in pairs(storage.ocfg.surfaces_blacklist) do
if (name == surface_name) then
return true
end
end
end
if (storage.ocfg.surfaces_blacklist_match ~= nil) then
for _,match in pairs(storage.ocfg.surfaces_blacklist_match) do
if (StringStartsWith(surface_name, match)) then
return true
end
end
end
return false
end
-- -- Simple way to write to a file. Always appends. Only server.
-- -- Has a global setting for enable/disable
-- function ServerWriteFile(filename, msg)
-- if (storage.ocfg.enable_server_write_files) then
-- game.write_file(filename, msg, true, 0)
-- end
-- end
---Useful for displaying game time in mins:secs format
---@param ticks number
---@return string
function FormatTime(ticks)
local seconds = ticks / 60
local minutes = math.floor((seconds)/60)
local seconds = math.floor(seconds - 60*minutes)
return string.format("%dm:%02ds", minutes, seconds)
end
---Useful for displaying game time in hrs:mins format
---@param ticks number
---@return string
function FormatTimeHoursSecs(ticks)
local seconds = ticks / 60
local total_minutes = math.floor((seconds)/60)
local hours = math.floor((total_minutes)/60)
local minutes = math.floor(total_minutes - 60*hours)
return string.format("%dh:%02dm", hours, minutes)
end
-- -- Simple math clamp
-- function clamp(val, min, max)
-- if (val > max) then
-- return max
-- elseif (val < min) then
-- return min
-- end
-- return val
-- end
-- function clampInt32(val)
-- return clamp(val, MAX_INT32_NEG, MAX_INT32_POS)
-- end
-- function MathRound(num)
-- return math.floor(num+0.5)
-- end
-- Fisher-Yares shuffle
-- https://stackoverflow.com/questions/35572435/how-do-you-do-the-fisher-yates-shuffle-in-lua
---@param T table
---@return table
function FYShuffle(T)
local tReturn = {}
for i = #T, 1, -1 do
local j = math.random(i)
T[i], T[j] = T[j], T[i]
table.insert(tReturn, T[i])
end
return tReturn
end
---Check if a table contains a value
---@param table table
---@param val any
---@return boolean
function TableContains(table, val)
for _, value in pairs(table) do
if value == val then
return true
end
end
return false
end
---Get a key from a table given a value (if it exists)
---@param table table
---@param val any
---@return any
function GetTableKey(table, val)
for k, v in pairs(table) do
if v == val then
return k
end
end
return nil
end
-- ---Remove a value from a table
-- ---@param table table
-- ---@param val any
-- ---@return nil
-- function TableRemove(t, val)
-- for i = #t, 1, -1 do
-- if t[i] == val then
-- table.remove(t, i)
-- end
-- end
-- end
function TableRemoveOneUsingPairs(t, val)
for k,v in pairs(t) do
if v == val then
table.remove(t, k)
return
end
end
end
---Gets a random point within a circle of a given radius and center point.
---@param radius number
---@param center MapPosition
---@return MapPosition
function GetRandomPointWithinCircle(radius, center)
local angle = math.random() * 2 * math.pi
local distance = math.random() * radius
local x = center.x + distance * math.cos(angle)
local y = center.y + distance * math.sin(angle)
return {x=x, y=y}
end
-- -- Get a random KEY from a table.
-- function GetRandomKeyFromTable(t)
-- local keyset = {}
-- for k,v in pairs(t) do
-- table.insert(keyset, k)
-- end
-- return keyset[math.random(#keyset)]
-- end
-- -- A safer way to attempt to get the next key in a table. CHECK TABLE SIZE BEFORE CALLING THIS!
-- -- Ensures the key points to a valid entry before calling next. Otherwise it restarts.
-- -- If you get nil as a return, it means you hit the return.
-- function NextButChecksKeyIsValidFirst(table_in, key)
-- -- if (table_size(table_in) == 0) then you're fucked end
-- if ((not key) or (not table_in[key])) then
-- return next(table_in, nil)
-- else
-- return next(table_in, key)
-- end
-- end
-- -- Gets the next key, even if we have to start again.
-- function NextKeyInTableIncludingRestart(table_in, key)
-- local next_key = NextButChecksKeyIsValidFirst(table_in, key)
-- if (not next_key) then
-- return NextButChecksKeyIsValidFirst(table_in, next_key)
-- else
-- return next_key
-- end
-- end
-- function GetRandomValueFromTable(t)
-- return t[GetRandomKeyFromTable(t)]
-- end
-- -- Given a table of positions, returns key for closest to given pos.
-- function GetClosestPosFromTable(pos, pos_table)
-- local closest_dist = nil
-- local closest_key = nil
-- for k,p in pairs(pos_table) do
-- local new_dist = util.distance(pos, p)
-- if (closest_dist == nil) then
-- closest_dist = new_dist
-- closest_key = k
-- elseif (closest_dist > new_dist) then
-- closest_dist = new_dist
-- closest_key = k
-- end
-- end
-- if (closest_key == nil) then
-- log("GetClosestPosFromTable ERROR - None found?")
-- return nil
-- end
-- return pos_table[closest_key]
-- end
-- Chart area for a force
---@param force string|integer|LuaForce
---@param position MapPosition
---@param chunkDist number
---@param surface LuaSurface|string|integer
function ChartArea(force, position, chunkDist, surface)
force.chart(surface,
{ { position.x - (CHUNK_SIZE * chunkDist),
position.y - (CHUNK_SIZE * chunkDist) },
{ position.x + (CHUNK_SIZE * chunkDist),
position.y + (CHUNK_SIZE * chunkDist) } })
end
--- Better than util.insert_safe because we also check for 0 count items.
---@param entity LuaEntity|LuaPlayer
---@param item_dict table
---@return nil
function OarcsSaferInsert(entity, item_dict)
if not (entity and entity.valid and item_dict) then return end
local items = prototypes.item
local insert = entity.insert
for name, count in pairs(item_dict) do
if items[name] and count > 0 then
insert { name = name, count = count }
else
log("Item to insert not valid: " .. name)
end
end
end
--- Better than util.remove_safe because we also check for 0 count items.
---@param entity LuaEntity|LuaPlayer
---@param item_dict table
---@return nil
function OarcsSaferRemove(entity, item_dict)
if not (entity and entity.valid and item_dict) then return end
local items = prototypes.item
local remove = entity.remove_item
for name, count in pairs(item_dict) do
if items[name] and count > 0 then
remove { name = name, count = count }
else
log("Item to remove not valid: " .. name)
end
end
end
---Gives the player the respawn items if there are any
---@param player LuaPlayer
---@return nil
function GivePlayerRespawnItems(player)
local surface_name = player.character.surface.name
if (storage.ocfg.surfaces_config[surface_name] == nil) then
error("GivePlayerRespawnItems - Missing surface config! " .. surface_name)
return
end
local respawnItems = storage.ocfg.surfaces_config[surface_name].starting_items.player_respawn_items
OarcsSaferInsert(player, respawnItems)
end
---Gives the player the starter items if there are any
---@param player LuaPlayer
---@return nil
function GivePlayerStarterItems(player)
local surface_name = player.character.surface.name
if (storage.ocfg.surfaces_config[surface_name] == nil) then
error("GivePlayerStarterItems - Missing surface config! " .. surface_name)
return
end
local startItems = storage.ocfg.surfaces_config[surface_name].starting_items.player_start_items
OarcsSaferInsert(player, startItems)
end
---Half-heartedly attempts to remove starter items from the player. Probably more trouble than it's worth.
---@param player LuaPlayer
---@return nil
function RemovePlayerStarterItems(player)
if player == nil or player.character == nil then return end
local surface_name = player.character.surface.name
if (storage.ocfg.surfaces_config[surface_name]) ~= nil then
local startItems = storage.ocfg.surfaces_config[surface_name].starting_items.player_start_items
OarcsSaferRemove(player, startItems)
end
end
--- Delete all chunks on a surface
--- @param surface LuaSurface
--- @return nil
function DeleteAllChunks(surface)
for chunk in surface.get_chunks() do
surface.delete_chunk({chunk.x, chunk.y})
end
end
---Get position for buddy spawn (for buddy placement)
---@param position MapPosition
---@param surface_name string
---@param moat_enabled boolean
---@return MapPosition
function GetBuddySpawnPosition(position, surface_name, moat_enabled)
local spawn_config = storage.ocfg.surfaces_config[surface_name].spawn_config
local x_offset = storage.ocfg.spawn_general.spawn_radius_tiles * spawn_config.radius_modifier * 2
x_offset = x_offset + storage.ocfg.spawn_general.moat_width_tiles
-- distance = distance + 5 -- EXTRA BUFFER?
-- Create that spawn in the global vars
local buddy_position = table.deepcopy(position)
-- The x_offset must be big enough to ensure the spawns DO NOT overlap!
buddy_position.x = buddy_position.x + x_offset
return buddy_position
end
-- -- Modular armor quick start
-- function GiveQuickStartModularArmor(player)
-- player.insert{name="modular-armor", count = 1}
-- if player and player.get_inventory(defines.inventory.character_armor) ~= nil and player.get_inventory(defines.inventory.character_armor)[1] ~= nil then
-- local p_armor = player.get_inventory(defines.inventory.character_armor)[1].grid
-- if p_armor ~= nil then
-- p_armor.put({name = "personal-roboport-equipment"})
-- p_armor.put({name = "battery-mk2-equipment"})
-- p_armor.put({name = "personal-roboport-equipment"})
-- for i=1,15 do
-- p_armor.put({name = "solar-panel-equipment"})
-- end
-- end
-- player.insert{name="construction-robot", count = 40}
-- end
-- end
-- -- Cheater's quick start
-- function GiveQuickStartPowerArmor(player)
-- player.insert{name="power-armor", count = 1}
-- if player and player.get_inventory(defines.inventory.character_armor) ~= nil and player.get_inventory(defines.inventory.character_armor)[1] ~= nil then
-- local p_armor = player.get_inventory(defines.inventory.character_armor)[1].grid
-- if p_armor ~= nil then
-- p_armor.put({name = "fusion-reactor-equipment"})
-- p_armor.put({name = "exoskeleton-equipment"})
-- p_armor.put({name = "battery-mk2-equipment"})
-- p_armor.put({name = "battery-mk2-equipment"})
-- p_armor.put({name = "personal-roboport-mk2-equipment"})
-- p_armor.put({name = "personal-roboport-mk2-equipment"})
-- p_armor.put({name = "personal-roboport-mk2-equipment"})
-- p_armor.put({name = "battery-mk2-equipment"})
-- for i=1,7 do
-- p_armor.put({name = "solar-panel-equipment"})
-- end
-- end
-- player.insert{name="construction-robot", count = 100}
-- player.insert{name="belt-immunity-equipment", count = 1}
-- end
-- end
-- TEST_KIT = {
-- {name="infinity-chest", count = 50},
-- {name="infinity-pipe", count = 50},
-- {name="electric-energy-interface", count = 50},
-- {name="express-loader", count = 50},
-- {name="express-transport-belt", count = 50},
-- }
-- function GiveTestKit(player)
-- for _,item in pairs(TEST_KIT) do
-- player.insert(item)
-- end
-- end
-- Safer teleport
---@param player LuaPlayer
---@param surface LuaSurface
---@param target_pos MapPosition
function SafeTeleport(player, surface, target_pos)
local safe_pos = surface.find_non_colliding_position("character", target_pos, CHUNK_SIZE, 1)
if (not safe_pos) then
player.teleport(target_pos, surface, true)
else
player.teleport(safe_pos, surface, true)
end
end
---Check if given position is in area bounding box
---@param point MapPosition
---@param area BoundingBox
---@return boolean
function CheckIfInArea(point, area)
if ((point.x >= area.left_top.x) and (point.x < area.right_bottom.x)) then
if ((point.y >= area.left_top.y) and (point.y < area.right_bottom.y)) then
return true
end
end
return false
end
---Configures the friend and cease fire relationships between all player forces.
---@param cease_fire boolean
---@param friends boolean
---@return nil
function ConfigurePlayerForceRelationships(cease_fire, friends)
local player_forces = {}
for name, force in pairs(game.forces) do
if name ~= ABANDONED_FORCE_NAME and not TableContains(ENEMY_FORCES_NAMES_INCL_NEUTRAL, name) then
table.insert(player_forces, force)
end
end
for _, force1 in pairs(player_forces) do
for _, force2 in pairs(player_forces) do
if force1.name ~= force2.name then
force1.set_cease_fire(force2, cease_fire)
force1.set_friend(force2, friends)
force2.set_cease_fire(force1, cease_fire)
force2.set_friend(force1, friends)
end
end
end
end
-- ---Set all forces to ceasefire
-- ---@return nil
-- function SetCeaseFireBetweenAllPlayerForces()
-- for name, team in pairs(game.forces) do
-- if name ~= "neutral" and name ~= ABANDONED_FORCE_NAME and not TableContains(ENEMY_FORCES_NAMES, name) then
-- for x, _ in pairs(game.forces) do
-- if x ~= "neutral" and x ~= ABANDONED_FORCE_NAME and not TableContains(ENEMY_FORCES_NAMES, x) then
-- team.set_cease_fire(x, true)
-- end
-- end
-- end
-- end
-- end
-- ---Set all forces to friendly
-- ---@return nil
-- function SetFriendlyBetweenAllPlayerForces()
-- for name, team in pairs(game.forces) do
-- if name ~= "neutral" and name ~= ABANDONED_FORCE_NAME and not TableContains(ENEMY_FORCES_NAMES, name) then
-- for x, _ in pairs(game.forces) do
-- if x ~= "neutral" and x ~= ABANDONED_FORCE_NAME and not TableContains(ENEMY_FORCES_NAMES, x) then
-- team.set_friend(x, true)
-- end
-- end
-- end
-- end
-- end
---For each other player force, share a chat msg.
---@param player LuaPlayer
---@param msg LocalisedString
---@return nil
function ShareChatBetweenForces(player, msg)
for _,force in pairs(game.forces) do
if (force ~= nil) then
if ((force.name ~= "enemy") and
(force.name ~= "neutral") and
(force.name ~= "player") and
(force ~= player.force)) then
force.print(player.name..": "..msg)
end
end
end
end
-- -- Merges force2 INTO force1 but keeps all research between both forces.
-- function MergeForcesKeepResearch(force1, force2)
-- for techName,luaTech in pairs(force2.technologies) do
-- if (luaTech.researched) then
-- force1.technologies[techName].researched = true
-- force1.technologies[techName].level = luaTech.level
-- end
-- end
-- game.merge_forces(force2, force1)
-- end
-- -- Undecorator
-- function RemoveDecorationsArea(surface, area)
-- surface.destroy_decoratives{area=area}
-- end
-- -- Remove fish
-- function RemoveFish(surface, area)
-- for _, entity in pairs(surface.find_entities_filtered{area = area, type="fish"}) do
-- entity.destroy()
-- end
-- end
-- -- Render a path
-- function RenderPath(path, ttl, players)
-- local last_pos = path[1].position
-- local color = {r = 1, g = 0, b = 0, a = 0.5}
-- for i,v in pairs(path) do
-- if (i ~= 1) then
-- color={r = 1/(1+(i%3)), g = 1/(1+(i%5)), b = 1/(1+(i%7)), a = 0.5}
-- rendering.draw_line{color=color,
-- width=2,
-- from=v.position,
-- to=last_pos,
-- surface=game.surfaces[GAME_SURFACE_NAME],
-- players=players,
-- time_to_live=ttl}
-- end
-- last_pos = v.position
-- end
-- end
---Get a random 1 or -1
---@return number
function RandomNegPos()
if (math.random(0,1) == 1) then
return 1
else
return -1
end
end
---Create a random direction vector to look in, returns normalized vector
---@return MapPosition
function GetRandomVector()
local randVec = {x=0,y=0}
while ((randVec.x == 0) and (randVec.y == 0)) do
randVec.x = math.random() * 2 - 1
randVec.y = math.random() * 2 - 1
end
-- Normalize the vector
local magnitude = math.sqrt((randVec.x^2) + (randVec.y^2))
randVec.x = randVec.x / magnitude
randVec.y = randVec.y / magnitude
return randVec
end
---Check for ungenerated chunks around a specific chunk +/- chunkDist in x and y directions
---@param chunkPos MapPosition
---@param chunkDist integer
---@param surface LuaSurface
---@return boolean
function IsChunkAreaUngenerated(chunkPos, chunkDist, surface)
for x=-chunkDist, chunkDist do
for y=-chunkDist, chunkDist do
local checkPos = {x=chunkPos.x+x,
y=chunkPos.y+y}
if (surface.is_chunk_generated(checkPos)) then
return false
end
end
end
return true
end
-- Clear out enemies around an area with a certain distance
---@param pos MapPosition
---@param safeDist number
---@param surface LuaSurface
function ClearNearbyEnemies(pos, safeDist, surface)
local safeArea = {
left_top =
{
x = pos.x - safeDist,
y = pos.y - safeDist
},
right_bottom =
{
x = pos.x + safeDist,
y = pos.y + safeDist
}
}
for _, entity in pairs(surface.find_entities_filtered { area = safeArea, force = "enemy" }) do
entity.destroy()
end
end
-- ---Function to find coordinates of ungenerated map area in a given direction starting from the center of the map
-- ---@param direction_vector MapPosition
-- ---@param surface LuaSurface
-- ---@return MapPosition
-- function FindMapEdge(direction_vector, surface)
-- local position = {x=0,y=0}
-- local chunk_position = {x=0,y=0}
-- -- Keep checking chunks in the direction of the vector
-- while(true) do
-- -- Set some absolute limits.
-- if ((math.abs(chunk_position.x) > 1000) or (math.abs(chunk_position.y) > 1000)) then
-- break
-- -- If chunk is already generated, keep looking
-- elseif (surface.is_chunk_generated(chunk_position)) then
-- chunk_position.x = chunk_position.x + direction_vector.x
-- chunk_position.y = chunk_position.y + direction_vector.y
-- -- Found a possible ungenerated area
-- else
-- chunk_position.x = chunk_position.x + direction_vector.x
-- chunk_position.y = chunk_position.y + direction_vector.y
-- -- Check there are no generated chunks in a 10x10 area.
-- if IsChunkAreaUngenerated(chunk_position, 10, surface) then
-- position.x = (chunk_position.x*CHUNK_SIZE) + (CHUNK_SIZE/2)
-- position.y = (chunk_position.y*CHUNK_SIZE) + (CHUNK_SIZE/2)
-- break
-- end
-- end
-- end
-- -- log("spawn: x=" .. position.x .. ", y=" .. position.y)
-- return position
-- end
---Pick a random direction, go at least the minimum distance, and start looking for ungenerated chunks
---We try a few times (hardcoded) and then try a different random direction if we fail (up to max_tries)
---@param surface_name string Surface name because we might need to force the creation of a new surface
---@param minimum_distance_chunks number Distance in chunks to start looking for ungenerated chunks
---@param max_tries integer Maximum number of tries to find a spawn point
---@return MapPosition
function FindUngeneratedCoordinates(surface_name, minimum_distance_chunks, max_tries)
local final_position = {x=0,y=0}
-- If surface is nil, it is probably a planet? Check and create if needed.
local surface = game.surfaces[surface_name]
if (surface == nil) then
if (game.planets[surface_name] == nil) then
error("ERROR! No surface or planet found for requested player spawn!")
return final_position
end
surface = game.planets[surface_name].create_surface()
if (surface == nil) then
error("ERROR! Failed to create planet surface for player spawn!")
return final_position
end
end
--- Get a random vector, figure out how many times to multiply it to get the minimum distance
local direction_vector = GetRandomVector()
local start_distance_tiles = minimum_distance_chunks * CHUNK_SIZE
local tries_remaining = max_tries - 1
-- Starting search position
local search_pos = {
x=direction_vector.x * start_distance_tiles,
y=direction_vector.y * start_distance_tiles
}
-- We check up to THIS many times, each jump moves out by minimum_distance_to_existing_chunks
local jumps_count = 3
local minimum_distance_to_existing_chunks = storage.ocfg.gameplay.minimum_distance_to_existing_chunks
-- Keep checking chunks in the direction of the vector, assumes this terminates...
while(true) do
local chunk_position = GetChunkPosFromTilePos(search_pos)
if (jumps_count <= 0) then
if (tries_remaining > 0) then
return FindUngeneratedCoordinates(surface_name, minimum_distance_chunks, tries_remaining)
else
log("WARNING - FindUngeneratedCoordinates - Hit max distance!")
break
end
-- If chunk is already generated, keep looking further out
elseif (surface.is_chunk_generated(chunk_position)) then
-- For debugging, ping the map
-- SendBroadcastMsg("GENERATED: " .. GetGPStext(surface.name, {x=chunk_position.x*32, y=chunk_position.y*32}))
-- Move out a bit more to give some space and then check the surrounding area
search_pos.x = search_pos.x + (direction_vector.x * CHUNK_SIZE * minimum_distance_to_existing_chunks)
search_pos.y = search_pos.y + (direction_vector.y * CHUNK_SIZE * minimum_distance_to_existing_chunks)
-- Found a possible ungenerated area
elseif IsChunkAreaUngenerated(chunk_position, minimum_distance_to_existing_chunks, surface) then
-- For debugging, ping the map
-- SendBroadcastMsg("SUCCESS: " .. GetGPStext(surface.name, {x=chunk_position.x*32, y=chunk_position.y*32}))
-- Place the spawn in the center of a chunk
final_position.x = (chunk_position.x * CHUNK_SIZE) + (CHUNK_SIZE/2)
final_position.y = (chunk_position.y * CHUNK_SIZE) + (CHUNK_SIZE/2)
break
-- The area around the chunk is not clear, keep looking
else
-- For debugging, ping the map
-- SendBroadcastMsg("NOT CLEAR: " .. GetGPStext(surface.name, {x=chunk_position.x*32, y=chunk_position.y*32}))
-- Move out a bit more to give some space and then check the surrounding area
search_pos.x = search_pos.x + (direction_vector.x * CHUNK_SIZE * minimum_distance_to_existing_chunks)
search_pos.y = search_pos.y + (direction_vector.y * CHUNK_SIZE * minimum_distance_to_existing_chunks)
end
jumps_count = jumps_count - 1
end
if (final_position.x == 0 and final_position.y == 0) then
log("WARNING! FindUngeneratedCoordinates - Failed to find a spawn point!")
end
return final_position
end
-- -- General purpose function for removing a particular recipe
-- function RemoveRecipe(force, recipeName)
-- local recipes = force.recipes
-- if recipes[recipeName] then
-- recipes[recipeName].enabled = false
-- end
-- end
-- -- General purpose function for adding a particular recipe
-- function AddRecipe(force, recipeName)
-- local recipes = force.recipes
-- if recipes[recipeName] then
-- recipes[recipeName].enabled = true
-- end
-- end
-- -- General command for disabling a tech.
-- function DisableTech(force, techName)
-- if force.technologies[techName] then
-- force.technologies[techName].enabled = false
-- force.technologies[techName].visible_when_disabled = true
-- end
-- end
-- -- General command for enabling a tech.
-- function EnableTech(force, techName)
-- if force.technologies[techName] then
-- force.technologies[techName].enabled = true
-- end
-- end
---Get a square area given a position and distance. Square length = 2x distance
---@param pos MapPosition
---@param dist number
---@return BoundingBox
function GetAreaAroundPos(pos, dist)
return {
left_top =
{
x = pos.x - dist,
y = pos.y - dist
},
right_bottom =
{
x = pos.x + dist,
y = pos.y + dist
}
}
end
---Gets chunk position of a tile.
---@param tile_pos TilePosition
---@return ChunkPosition
function GetChunkPosFromTilePos(tile_pos)
return {x=math.floor(tile_pos.x/32), y=math.floor(tile_pos.y/32)}
end
-- function GetCenterTilePosFromChunkPos(c_pos)
-- return {x=c_pos.x*32 + 16, y=c_pos.y*32 + 16}
-- end
-- -- Get the left_top
-- function GetChunkTopLeft(pos)
-- return {x=pos.x-(pos.x % 32), y=pos.y-(pos.y % 32)}
-- end
-- -- Get area given chunk
-- function GetAreaFromChunkPos(chunk_pos)
-- return {left_top={x=chunk_pos.x*32, y=chunk_pos.y*32},
-- right_bottom={x=chunk_pos.x*32+31, y=chunk_pos.y*32+31}}
-- end
-- Removes the entity type from the area given
-- function RemoveInArea(surface, area, type)
-- for key, entity in pairs(surface.find_entities_filtered{area=area, type= type}) do
-- if entity.valid and entity and entity.position then
-- entity.destroy()
-- end
-- end
-- end
---Removes the entity type from the area given. Only if it is within given distance from given position.
---@param surface LuaSurface
---@param area BoundingBox
---@param type string|string[]
---@param pos MapPosition
---@param dist number
---@return nil
function RemoveInCircle(surface, area, type, pos, dist)
for _, entity in pairs(surface.find_entities_filtered { area = area, type = type }) do
if entity.valid and entity and entity.position then
if ((pos.x - entity.position.x) ^ 2 + (pos.y - entity.position.y) ^ 2 < dist ^ 2) then
entity.destroy()
end
end
end
end
---Removes the entity type from the area given. Only if it is within given distance from given position.
---@param surface LuaSurface
---@param area BoundingBox
---@param type string|string[]
---@param pos MapPosition
---@param dist number
---@return nil
function RemoveInSquare(surface, area, type, pos, dist)
for _, entity in pairs(surface.find_entities_filtered { area = area, type = type }) do
if entity.valid and entity and entity.position then
local max_distance = math.max(math.abs(pos.x - entity.position.x), math.abs(pos.y - entity.position.y))
if (max_distance < dist) then
entity.destroy()
end
end
end
end
-- -- For easy local testing of map gen settings. Just set what you want and uncomment usage in CreateGameSurface!
-- function SurfaceSettingsHelper(settings)
-- settings.terrain_segmentation = 4
-- settings.water = 3
-- settings.starting_area = 0
-- local r_freq = 1.20
-- local r_rich = 5.00
-- local r_size = 0.18
-- settings.autoplace_controls["coal"].frequency = r_freq
-- settings.autoplace_controls["coal"].richness = r_rich
-- settings.autoplace_controls["coal"].size = r_size
-- settings.autoplace_controls["copper-ore"].frequency = r_freq
-- settings.autoplace_controls["copper-ore"].richness = r_rich
-- settings.autoplace_controls["copper-ore"].size = r_size
-- settings.autoplace_controls["crude-oil"].frequency = r_freq
-- settings.autoplace_controls["crude-oil"].richness = r_rich
-- settings.autoplace_controls["crude-oil"].size = r_size
-- settings.autoplace_controls["iron-ore"].frequency = r_freq
-- settings.autoplace_controls["iron-ore"].richness = r_rich
-- settings.autoplace_controls["iron-ore"].size = r_size
-- settings.autoplace_controls["stone"].frequency = r_freq
-- settings.autoplace_controls["stone"].richness = r_rich
-- settings.autoplace_controls["stone"].size = r_size
-- settings.autoplace_controls["uranium-ore"].frequency = r_freq*0.5
-- settings.autoplace_controls["uranium-ore"].richness = r_rich
-- settings.autoplace_controls["uranium-ore"].size = r_size
-- settings.autoplace_controls["enemy-base"].frequency = 0.80
-- settings.autoplace_controls["enemy-base"].richness = 0.70
-- settings.autoplace_controls["enemy-base"].size = 0.70
-- settings.autoplace_controls["trees"].frequency = 1.00
-- settings.autoplace_controls["trees"].richness = 1.00
-- settings.autoplace_controls["trees"].size = 1.00
-- settings.cliff_settings.cliff_elevation_0 = 3
-- settings.cliff_settings.cliff_elevation_interval = 200
-- settings.cliff_settings.richness = 3
-- settings.property_expression_names["control-setting:aux:bias"] = "0.00"
-- settings.property_expression_names["control-setting:aux:frequency:multiplier"] = "5.00"
-- settings.property_expression_names["control-setting:moisture:bias"] = "0.40"
-- settings.property_expression_names["control-setting:moisture:frequency:multiplier"] = "50"
-- return settings
-- end
-- -- Create another surface so that we can modify map settings and not have a screwy nauvis map.
-- function CreateGameSurface()
-- if (GAME_SURFACE_NAME ~= "nauvis") then
-- -- Get starting surface settings.
-- local nauvis_settings = game.surfaces["nauvis"].map_gen_settings
-- if storage.ocfg.enable_vanilla_spawns then
-- nauvis_settings.starting_points = CreateVanillaSpawns(storage.ocfg.vanilla_spawn_count, storage.ocfg.vanilla_spawn_spacing)
-- -- ENFORCE ISLAND MAP GEN
-- if (storage.ocfg.silo_islands) then
-- nauvis_settings.property_expression_names.elevation = "0_17-island"
-- end
-- end
-- -- Enable this to test things out easily.
-- -- nauvis_settings = SurfaceSettingsHelper(nauvis_settings)
-- -- Create new game surface
-- local s = game.create_surface(GAME_SURFACE_NAME, nauvis_settings)
-- end
-- -- Add surface and safe areas
-- if storage.ocfg.enable_regrowth then
-- RegrowthMarkAreaSafeGivenChunkPos({x=0,y=0}, 4, true)
-- end
-- end
-- function CreateTileArrow(surface, pos, type)
-- tiles = {}
-- if (type == "LEFT") then
-- table.insert(tiles, {name = "hazard-concrete-left", position = {pos.x, pos.y}})
-- table.insert(tiles, {name = "hazard-concrete-left", position = {pos.x+1, pos.y}})
-- table.insert(tiles, {name = "hazard-concrete-left", position = {pos.x+2, pos.y}})
-- table.insert(tiles, {name = "hazard-concrete-left", position = {pos.x+3, pos.y}})
-- table.insert(tiles, {name = "hazard-concrete-right", position = {pos.x, pos.y+1}})
-- table.insert(tiles, {name = "hazard-concrete-right", position = {pos.x+1, pos.y+1}})
-- table.insert(tiles, {name = "hazard-concrete-right", position = {pos.x+2, pos.y+1}})
-- table.insert(tiles, {name = "hazard-concrete-right", position = {pos.x+3, pos.y+1}})
-- elseif (type == "RIGHT") then
-- table.insert(tiles, {name = "hazard-concrete-right", position = {pos.x, pos.y}})
-- table.insert(tiles, {name = "hazard-concrete-right", position = {pos.x+1, pos.y}})
-- table.insert(tiles, {name = "hazard-concrete-right", position = {pos.x+2, pos.y}})
-- table.insert(tiles, {name = "hazard-concrete-right", position = {pos.x+3, pos.y}})
-- table.insert(tiles, {name = "hazard-concrete-left", position = {pos.x, pos.y+1}})
-- table.insert(tiles, {name = "hazard-concrete-left", position = {pos.x+1, pos.y+1}})
-- table.insert(tiles, {name = "hazard-concrete-left", position = {pos.x+2, pos.y+1}})
-- table.insert(tiles, {name = "hazard-concrete-left", position = {pos.x+3, pos.y+1}})
-- end
-- surface.set_tiles(tiles, true)
-- end
-- -- Allowed colors: red, green, blue, orange, yellow, pink, purple, black, brown, cyan, acid
-- function CreateFixedColorTileArea(surface, area, color)
-- tiles = {}
-- for i=area.left_top.x,area.right_bottom.x do
-- for j=area.left_top.y,area.right_bottom.y do
-- table.insert(tiles, {name = color.."-refined-concrete", position = {i,j}})
-- end
-- end
-- surface.set_tiles(tiles, true)
-- end
-- -- Find closest player-owned entity
-- function FindClosestPlayerOwnedEntity(player, name, radius)
-- local entities = player.surface.find_entities_filtered{position=player.position,
-- radius=radius,
-- name=name,
-- force=player.force}
-- if (not entities or (#entities == 0)) then return nil end
-- return player.surface.get_closest(player.position, entities)
-- end
-- -- Add Long Reach to Character
-- function GivePlayerLongReach(player)
-- player.character.character_build_distance_bonus = BUILD_DIST_BONUS
-- player.character.character_reach_distance_bonus = REACH_DIST_BONUS
-- -- player.character.character_resource_reach_distance_bonus = RESOURCE_DIST_BONUS
-- end
-- -- General purpose cover an area in tiles.
-- function CoverAreaInTiles(surface, area, tile_name)
-- tiles = {}
-- for x = area.left_top.x,area.left_top.x+31 do
-- for y = area.left_top.y,area.left_top.y+31 do
-- table.insert(tiles, {name = tile_name, position = {x=x, y=y}})
-- end
-- end
-- surface.set_tiles(tiles, true)
-- end
-- --------------------------------------------------------------------------------
-- -- Anti-griefing Stuff & Gravestone (My own version)
-- --------------------------------------------------------------------------------
-- function AntiGriefing(force)
-- force.zoom_to_world_deconstruction_planner_enabled=false
-- SetForceGhostTimeToLive(force)
-- end
-- function SetForceGhostTimeToLive(force)
-- if storage.ocfg.ghost_ttl ~= 0 then
-- force.ghost_time_to_live = storage.ocfg.ghost_ttl+1
-- end
-- end
-- function SetItemBlueprintTimeToLive(event)
-- local type = event.created_entity.type
-- if type == "entity-ghost" or type == "tile-ghost" then
-- if storage.ocfg.ghost_ttl ~= 0 then
-- event.created_entity.time_to_live = storage.ocfg.ghost_ttl
-- end
-- end
-- end
-- --------------------------------------------------------------------------------
-- -- Resource patch and starting area generation
-- --------------------------------------------------------------------------------
---Circle spawn shape (handles land, trees and moat)
---@param surface LuaSurface
---@param centerPos MapPosition
---@param chunkArea BoundingBox
---@param tileRadius number
---@param fillTile string
---@param moat boolean
---@param bridge boolean
---@return nil
function CreateCropCircle(surface, centerPos, chunkArea, tileRadius, fillTile, moat, bridge)
local tile_radius_sqr = tileRadius ^ 2
local moat_width = storage.ocfg.spawn_general.moat_width_tiles
local moat_radius_sqr = ((tileRadius + moat_width)^2)
local tree_width = storage.ocfg.spawn_general.tree_width_tiles
local tree_radius_sqr_inner = ((tileRadius - 1 - tree_width) ^ 2) -- 1 less to make sure trees are inside the spawn area
local tree_radius_sqr_outer = ((tileRadius - 1) ^ 2)
local surface_config = storage.ocfg.surfaces_config[surface.name]
local liquid_tile = surface_config.spawn_config.liquid_tile
local fish_enabled = (liquid_tile == "water")
local dirtTiles = {}
for i = chunkArea.left_top.x, chunkArea.right_bottom.x, 1 do
for j = chunkArea.left_top.y, chunkArea.right_bottom.y, 1 do
-- This ( X^2 + Y^2 ) is used to calculate if something is inside a circle area.
-- We avoid using sqrt for performance reasons.
local distSqr = math.floor((centerPos.x - i) ^ 2 + (centerPos.y - j) ^ 2)
-- Fill in all unexpected water (or force grass)
if (distSqr <= tile_radius_sqr) then
if (surface.get_tile(i, j).collides_with("water_tile") or
storage.ocfg.spawn_general.force_grass) then
table.insert(dirtTiles, { name = fillTile, position = { i, j } })
end
end
-- -- Create a tree ring
-- if ((distSqr < tree_radius_sqr_outer) and (distSqr > tree_radius_sqr_inner)) then
-- surface.create_entity({ name = "tree-02", amount = 1, position = { i, j } })
-- end
-- Fill moat with water.
if (moat) then
if (bridge and ((j == centerPos.y - 1) or (j == centerPos.y) or (j == centerPos.y + 1))) then
-- This will leave the tiles "as is" on the left and right of the spawn which has the effect of creating
-- land connections if the spawn is on or near land.
elseif ((distSqr < moat_radius_sqr) and (distSqr > tile_radius_sqr)) then
table.insert(dirtTiles, { name = liquid_tile, position = { i, j } })
--5% chance of fish in water
if fish_enabled and (math.random(1,20) == 1) then
surface.create_entity({name="fish", position={i + 0.5, j + 0.5}})
end
end
end
end
end
surface.set_tiles(dirtTiles)
--Create trees (needs to be done after setting tiles!)
local tree_entity = surface_config.spawn_config.tree_entity
if (tree_entity == nil) then return end
for i = chunkArea.left_top.x, chunkArea.right_bottom.x, 1 do
for j = chunkArea.left_top.y, chunkArea.right_bottom.y, 1 do
local distSqr = math.floor((centerPos.x - i) ^ 2 + (centerPos.y - j) ^ 2)
if ((distSqr < tree_radius_sqr_outer) and (distSqr > tree_radius_sqr_inner)) then
local pos = surface.find_non_colliding_position(tree_entity, { i, j }, 2, 0.5)
if (pos ~= nil) then
surface.create_entity({ name = tree_entity, amount = 1, position = pos })
end
-- surface.create_entity({ name = "tree-02", amount = 1, position = { i, j } })
end
end
end
end
---` spawn shape (handles land, trees and moat) (Curtesy of jvmguy)
---@param surface LuaSurface
---@param centerPos MapPosition
---@param chunkArea BoundingBox
---@param tileRadius number
---@param fillTile string
---@param moat boolean
---@param bridge boolean
---@return nil
function CreateCropOctagon(surface, centerPos, chunkArea, tileRadius, fillTile, moat, bridge)
local moat_width = storage.ocfg.spawn_general.moat_width_tiles
local moat_width_outer = tileRadius + moat_width
local tree_width = storage.ocfg.spawn_general.tree_width_tiles
local tree_distance_inner = tileRadius - tree_width
local surface_config = storage.ocfg.surfaces_config[surface.name]
local dirtTiles = {}
for i = chunkArea.left_top.x, chunkArea.right_bottom.x, 1 do
for j = chunkArea.left_top.y, chunkArea.right_bottom.y, 1 do
local distVar1 = math.floor(math.max(math.abs(centerPos.x - i), math.abs(centerPos.y - j)))
local distVar2 = math.floor(math.abs(centerPos.x - i) + math.abs(centerPos.y - j))
local distVar = math.max(distVar1, distVar2 * 0.707);
-- Fill in all unexpected water (or force grass)
if (distVar <= tileRadius) then
if (surface.get_tile(i, j).collides_with("water_tile") or
storage.ocfg.spawn_general.force_grass) then
table.insert(dirtTiles, { name = fillTile, position = { i, j } })
end
end
-- -- Create a tree ring
-- if ((distVar < tileRadius) and (distVar >= tree_distance_inner)) then
-- surface.create_entity({ name = "tree-01", amount = 1, position = { i, j } })
-- end
-- Fill moat with water
if (moat) then
if (bridge and ((j == centerPos.y - 1) or (j == centerPos.y) or (j == centerPos.y + 1))) then
-- This will leave the tiles "as is" on the left and right of the spawn which has the effect of creating
-- land connections if the spawn is on or near land.
elseif ((distVar > tileRadius) and (distVar <= moat_width_outer)) then
table.insert(dirtTiles, { name = "water", position = { i, j } })
--5% chance of fish in water
if (math.random(1,20) == 1) then
surface.create_entity({name="fish", position={i + 0.5, j + 0.5}})
end
end
end
end
end
surface.set_tiles(dirtTiles)
--Create trees (needs to be done after setting tiles!)
local tree_entity = surface_config.spawn_config.tree_entity
if (tree_entity == nil) then return end
--Create trees (needs to be done after setting tiles!)
for i = chunkArea.left_top.x, chunkArea.right_bottom.x, 1 do
for j = chunkArea.left_top.y, chunkArea.right_bottom.y, 1 do
local distVar1 = math.floor(math.max(math.abs(centerPos.x - i), math.abs(centerPos.y - j)))
local distVar2 = math.floor(math.abs(centerPos.x - i) + math.abs(centerPos.y - j))
local distVar = math.max(distVar1, distVar2 * 0.707);
if ((distVar < tileRadius) and (distVar >= tree_distance_inner)) then
surface.create_entity({ name = "tree-01", amount = 1, position = { i, j } })
end
end
end
end
---Square spawn shape (handles land, trees and moat)
---@param surface LuaSurface
---@param centerPos MapPosition
---@param chunkArea BoundingBox
---@param tileRadius number
---@param fillTile string
---@param moat boolean
---@param bridge boolean
---@return nil
function CreateCropSquare(surface, centerPos, chunkArea, tileRadius, fillTile, moat, bridge)
local moat_width = storage.ocfg.spawn_general.moat_width_tiles
local moat_width_outer = tileRadius + moat_width
local tree_width = storage.ocfg.spawn_general.tree_width_tiles
local tree_distance_inner = tileRadius - tree_width
local surface_config = storage.ocfg.surfaces_config[surface.name]
local dirtTiles = {}
for i = chunkArea.left_top.x, chunkArea.right_bottom.x, 1 do
for j = chunkArea.left_top.y, chunkArea.right_bottom.y, 1 do
-- Max distance from center (either x or y)
local max_distance = math.max(math.abs(centerPos.x - i), math.abs(centerPos.y - j))
-- Fill in all unexpected water (or force grass)
if (max_distance <= tileRadius) then
if (surface.get_tile(i, j).collides_with("water_tile") or
storage.ocfg.spawn_general.force_grass) then
table.insert(dirtTiles, { name = fillTile, position = { i, j } })
end
end
-- -- Create a tree ring
-- if ((max_distance < tileRadius) and (max_distance >= tree_distance_inner)) then
-- surface.create_entity({ name = "tree-02", amount = 1, position = { i, j } })
-- end
-- Fill moat with water
if (moat) then
if (bridge and ((j == centerPos.y - 1) or (j == centerPos.y) or (j == centerPos.y + 1))) then
-- This will leave the tiles "as is" on the left and right of the spawn which has the effect of creating
-- land connections if the spawn is on or near land.
elseif ((max_distance > tileRadius) and (max_distance <= moat_width_outer)) then
table.insert(dirtTiles, { name = "water", position = { i, j } })
--5% chance of fish in water
if (math.random(1,20) == 1) then
surface.create_entity({name="fish", position={i + 0.5, j + 0.5}})
end
end
end
end
end
surface.set_tiles(dirtTiles)
--Create trees (needs to be done after setting tiles!)
local tree_entity = surface_config.spawn_config.tree_entity
if (tree_entity == nil) then return end
--Create trees (needs to be done after setting tiles!)
for i = chunkArea.left_top.x, chunkArea.right_bottom.x, 1 do
for j = chunkArea.left_top.y, chunkArea.right_bottom.y, 1 do
local max_distance = math.max(math.abs(centerPos.x - i), math.abs(centerPos.y - j))
if ((max_distance < tileRadius) and (max_distance >= tree_distance_inner)) then
surface.create_entity({ name = "tree-02", amount = 1, position = { i, j } })
end
end
end
end
---Add a circle of water
---@param surface LuaSurface
---@param centerPos MapPosition
---@param chunkArea BoundingBox
---@param tileRadius number
---@param moatTile string
---@param bridge boolean
---@param shape SpawnShapeChoice
---@return nil
function CreateMoat(surface, centerPos, chunkArea, tileRadius, moatTile, bridge, shape)
local tileRadSqr = tileRadius ^ 2
local tiles = {}
for i = chunkArea.left_top.x, chunkArea.right_bottom.x, 1 do
for j = chunkArea.left_top.y, chunkArea.right_bottom.y, 1 do
if (bridge and ((j == centerPos.y - 1) or (j == centerPos.y) or (j == centerPos.y + 1))) then
-- This will leave the tiles "as is" on the left and right of the spawn which has the effect of creating
-- land connections if the spawn is on or near land.
else
-- This ( X^2 + Y^2 ) is used to calculate if something
-- is inside a circle area.
local distVar = math.floor((centerPos.x - i) ^ 2 + (centerPos.y - j) ^ 2)
-- Create a circle of water
if ((distVar < tileRadSqr + (1500 * storage.ocfg.spawn_general.moat_width_tiles)) and
(distVar > tileRadSqr)) then
table.insert(tiles, { name = moatTile, position = { i, j } })
end
end
end
end
surface.set_tiles(tiles)
end
-- Create a horizontal line of tiles (typically used for water)
---@param surface LuaSurface
---@param leftPos TilePosition
---@param length integer
---@param tile_name string
---@return nil
function CreateTileStrip(surface, leftPos, length, tile_name)
local waterTiles = {}
for i = 0, length-1, 1 do
table.insert(waterTiles, { name = tile_name, position = { leftPos.x + i, leftPos.y } })
end
surface.set_tiles(waterTiles)
end
--- Function to generate a resource patch, of a certain size/amount at a pos.
---@param surface LuaSurface
---@param resourceName string
---@param diameter integer
---@param position TilePosition
---@param amount integer
function GenerateResourcePatch(surface, resourceName, diameter, position, amount)
local midPoint = math.floor(diameter / 2)
if (diameter == 0) then
return
end
-- Right now only 2 shapes are supported. Circle and Square.
local square_shape = (storage.ocfg.spawn_general.resources_shape == RESOURCES_SHAPE_CHOICE_SQUARE)
for y = -midPoint, midPoint do
for x = -midPoint, midPoint do
-- Either it's a square, or it's a circle so we check if it's inside the circle.
if (square_shape or ((x) ^ 2 + (y) ^ 2 < midPoint ^ 2)) then
surface.create_entity({
name = resourceName,
amount = amount,
position = { position.x + x, position.y + y }
})
end
end
end
end
--- Function to generate a resource patch, of a certain size/amount at a pos.
---@param surface LuaSurface
---@param position MapPosition
---@return nil
function PlaceRandomEntities(surface, position)
local spawn_config = storage.ocfg.surfaces_config[surface.name].spawn_config
local random_entities = spawn_config.random_entities
if (random_entities == nil) then return end
local tree_width = storage.ocfg.spawn_general.tree_width_tiles
local radius = storage.ocfg.spawn_general.spawn_radius_tiles * spawn_config.radius_modifier - tree_width
--Iterate through the random entities and place them
for _, entry in pairs(random_entities) do
local entity_name = entry.name
for i = 1, entry.count do
local random_pos = GetRandomPointWithinCircle(radius, position)
local open_pos = surface.find_non_colliding_position(entity_name, random_pos, tree_width, 0.5)
if (open_pos ~= nil) then
surface.create_entity({
name = entity_name,
position = open_pos
})
end
end
end
end
--- Randomly place lightning attractors specific for Fulgora. This should space them out so they don't overlap too much.
---@param surface LuaSurface
---@param position MapPosition
---@return nil
function PlaceFulgoranLightningAttractors(surface, position, count)
local spawn_config = storage.ocfg.surfaces_config[surface.name].spawn_config
local radius = storage.ocfg.spawn_general.spawn_radius_tiles * spawn_config.radius_modifier
-- HARDCODED FOR NOW
local ATTRACTOR_NAME = "fulgoran-ruin-attractor"
local ATTRACTOR_RADIUS = 20
--Iterate through and place them and use the largest available entity
for i = 1, count do
local random_pos = GetRandomPointWithinCircle(radius, position)
local open_pos = surface.find_non_colliding_position("crash-site-spaceship", random_pos, 1, 0.5)
if (open_pos ~= nil) then
surface.create_entity({
name = ATTRACTOR_NAME,
position = open_pos,
force = "player" -- Same as native game
})
end
end
end
-- --------------------------------------------------------------------------------
-- -- EVENT SPECIFIC FUNCTIONS
-- --------------------------------------------------------------------------------
-- -- Display messages to a user everytime they join
-- function PlayerJoinedMessages(event)
-- local player = game.players[event.player_index]
-- player.print(storage.ocfg.welcome_msg)
-- if (storage.oarc_announcements) then
-- player.print(storage.oarc_announcements)
-- end
-- end
-- -- Remove decor to save on file size
-- function UndecorateOnChunkGenerate(event)
-- local surface = event.surface
-- local chunkArea = event.area
-- RemoveDecorationsArea(surface, chunkArea)
-- -- If you care to, you can remove all fish with the Undecorator option here:
-- -- RemoveFish(surface, chunkArea)
-- end