diff --git a/config.lua b/config.lua new file mode 100644 index 0000000..d5681a0 --- /dev/null +++ b/config.lua @@ -0,0 +1,119 @@ +-- config.lua +-- Configuration Options + +-------------------------------------------------------------------------------- +-- Useful constants +-------------------------------------------------------------------------------- +CHUNK_SIZE = 32 +MAX_FORCES = 64 +-------------------------------------------------------------------------------- + + +WELCOME_MSG = "Welcome to Oarc's server! This is a BETA test of my custom scenario." +GAME_MODE_MSG = "In the current game mode, a satellite must be launched from the rocket silo to the far east to win!" +-- GAME_MODE_MSG = "The current game mode is just basic vanilla!" +MODULES_ENABLED = "Mods Enabled: Separate Spawns, RSO, Gravestone Chests" +-- MODULES_ENABLED = "Mods Enabled: Gravestone-Chests" + +-------------------------------------------------------------------------------- +-- Module Enables +-- These enables are not fully tested! For example, disable separate spawns +-- will probably break the frontier rocket silo mode +-------------------------------------------------------------------------------- + +-- Separate spawns +FRONTIER_ROCKET_SILO_MODE = true + +-- Frontier style rocket silo mode +ENABLE_SEPARATE_SPAWNS = true + +-- Enable Scenario version of RSO +ENABLE_RSO = true + +-- Enable Gravestone Chests +ENABLE_GRAVESTONE_CHESTS = true + +-- Enable Undecorator +ENABLE_UNDECORATOR = true + +-- DEBUG prints for me +global.oarcDebugEnabled = false +global.spawnDebugEnabled = false + +-------------------------------------------------------------------------------- +-- Spawn Options +-------------------------------------------------------------------------------- + +--------------------------------------- +-- Distance Options +--------------------------------------- + +-- Distance in chunks +FAR_MIN_DIST = 50 +FAR_MAX_DIST = 125 +--------------------------------------- +-- Resource Options +--------------------------------------- + +-- Start resource amounts +START_IRON_AMOUNT = 1500 +START_COPPER_AMOUNT = 1500 +START_STONE_AMOUNT = 1500 +START_COAL_AMOUNT = 1500 +START_OIL_AMOUNT = 30000 + + +--------------------------------------- +-- Safe Spawn Area Options +--------------------------------------- + +-- Safe area has no aliens +-- +/- this in x and y direction +SAFE_AREA_TILE_DIST = 300 + +-- Warning area has reduced aliens +-- +/- this in x and y direction +WARNING_AREA_TILE_DIST = 500 + +-- 1 : X (spawners alive : spawners destroyed) in this area +WARN_AREA_REDUCTION_RATIO = 15 + +-- Create a circle of land area for the spawn +ENFORCE_LAND_AREA_TILE_DIST = 40 + + +--------------------------------------- +-- Other Forces/Teams Options +--------------------------------------- + +-- Enable if people can join their own teams +ENABLE_OTHER_TEAMS = false + +-- Main force is what default players join +MAIN_FORCE = "main_force" + + +--------------------------------------- +-- Alien Options +--------------------------------------- + +-- Disable enemy expansion +ENEMY_EXPANSION = false + + +-- Divide the alien factors by this number to reduce it (or multiply if < 1) +ENEMY_POLLUTION_FACTOR_DIVISOR = 10 +ENEMY_DESTROY_FACTOR_DIVISOR = 2 + + + +-------------------------------------------------------------------------------- +-- Frontier Options +-------------------------------------------------------------------------------- + +SILO_CHUNK_DISTANCE_X = 250 +SILO_DISTANCE_X = SILO_CHUNK_DISTANCE_X*CHUNK_SIZE + CHUNK_SIZE/2 +SILO_DISTANCE_Y = 16 + +-- Should be in the middle of a chunk +SILO_POSITION = {x = SILO_DISTANCE_X, y = SILO_DISTANCE_Y} diff --git a/control.lua b/control.lua index 93d13b7..b2449b5 100755 --- a/control.lua +++ b/control.lua @@ -13,903 +13,39 @@ -- if they were not already. --- TODO --- Anti griefer? Drop all items on leaving? --- Better long term goal like frontier --- Better interaction with other forces? +-- To do: +-- Clean up text around spawn choices. +-- Make rocket silo more obvious, try to spawn a train station +-- Add longreach --- config options --- Near spawn option is on the edge of generated chunks --- On a large map, this may be quite far from spawn. -local MIN_CHUNK_SPAWN_DIST = 2 - --- Far spawn options is this number of chunks past generated area -local FAR_CHUNK_SPAWN_DIST = 10 - -local FAR_MIN_DIST = 1000^2 -local FAR_MAX_DIST = 6000^2 - --- Start resource amountsmm -local START_IRON_AMOUNT = 1500 -local START_COPPER_AMOUNT = 1000 -local START_STONE_AMOUNT = 1500 -local START_COAL_AMOUNT = 1500 -local START_OIL_AMOUNT = 20000 - --- Safe area has no aliens --- +/- this in x and y direction -local SAFE_AREA_TILE_DIST = 250 - --- Warning area has reduced aliens --- +/- this in x and y direction -local WARNING_AREA_TILE_DIST = 500 - --- 1 : X (spawners alive : spawners destroyed) in this area -local WARN_AREA_REDUCTION_RATIO = 15 - --- Create a circle of land area for the spawn -local ENFORCE_LAND_AREA_TILE_DIST = 40 -local ENFORCE_LAND_AREA_TILE_DIST_SQUARED = ENFORCE_LAND_AREA_TILE_DIST^2 - --- Main force is what default players join -local MAIN_FORCE = "main_force" -local ENABLE_OTHER_TEAMS = false - --- Disable enemy expansion -local ENEMY_EXPANSION = false - --- Divide the alien factors by this number to reduce it (or multiply if < 1) -local ENEMY_POLLUTION_FACTOR_DIVISOR = 10 -local ENEMY_DESTROY_FACTOR_DIVISOR = 1 - --- Useful constants -local CHUNK_SIZE = 32 -local MAX_FORCES = 64 - - --- Print debug only to me while testing. --- Should remove this if you are hosting it yourself. -local function DebugPrint(msg) - if ((game.players["Oarc"] ~= nil) and (global.oarcDebugEnabled)) then - game.players["Oarc"].print("DEBUG: " .. msg) - end -end - - --- Simple function to get total number of items in table -function TableLength(T) - local count = 0 - for _ in pairs(T) do count = count + 1 end - return count -end - --- Give player these default items. -local function GivePlayerItems(player) - player.insert{name="pistol", count=1} - player.insert{name="firearm-magazine", count=10} -end - --- Additional starter only items -local function GivePlayerStarterItems(player) - GivePlayerItems(player) - player.insert{name="iron-plate", count=8} - player.insert{name="burner-mining-drill", count = 1} - player.insert{name="stone-furnace", count = 1} -end - --- Check if given position is in area bounding box -local 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 - --- Ceasefire --- All forces are always neutral -local function SetCeaseFireBetweenAllForces() - for name,team in pairs(game.forces) do - if name ~= "neutral" and name ~= "enemy" then - for x,y in pairs(game.forces) do - if x ~= "neutral" and x ~= "enemy" then - team.set_cease_fire(x,true) - end - end - end - end -end - --- Broadcast messages -local function SendBroadcastMsg(msg) - for name,player in pairs(game.players) do - player.print(msg) - end -end - - -local function CreateNewSpawnCoordinates(spawn_distance, recursion_max) - local position = {x=0,y=0} - local chunkPos = {x=0,y=0} - local randVec = {x=0,y=0} - - -- Create a random direction vector to look in - while ((randVec.x == 0) and (randVec.y == 0)) - do - randVec.x = math.random(-3,3) - randVec.y = math.random(-3,3) - end - DebugPrint("direction: x=" .. randVec.x .. ", y=" .. randVec.y) - - while(true) - do - - -- Set some absolute limits. - if ((math.abs(chunkPos.x) > 1000) or (math.abs(chunkPos.y) > 1000)) then - break - - -- If chunk is already generated, keep looking - elseif (game.surfaces["nauvis"].is_chunk_generated(chunkPos)) then - chunkPos.x = chunkPos.x + randVec.x*spawn_distance - chunkPos.y = chunkPos.y + randVec.y*spawn_distance - - -- Found a possible ungenerated area - else - - chunkPos.x = chunkPos.x + (randVec.x*spawn_distance) - chunkPos.y = chunkPos.y + (randVec.y*spawn_distance) - - -- If it's still ungenerated even further out, use that position. - if (not game.surfaces["nauvis"].is_chunk_generated(chunkPos)) then - position.x = (chunkPos.x*CHUNK_SIZE) + (CHUNK_SIZE/2) - position.y = (chunkPos.y*CHUNK_SIZE) + (CHUNK_SIZE/2) - break - end - end - end - - -- Very dangerous and stupid recursive call. - if (spawn_distance == FAR_CHUNK_SPAWN_DIST) then - local distSqrd = position.x^2 + position.y^2 - if ((distSqrd < FAR_MIN_DIST) or (distSqrd > FAR_MAX_DIST)) then - if (recursion_max == 0) then - return {x=0,y=0} - else - DebugPrint("spawn: x=" .. position.x .. ", y=" .. position.y) - return CreateNewSpawnCoordinates(spawn_distance, recursion_max-1) - end - end - end - - DebugPrint("spawn: x=" .. position.x .. ", y=" .. position.y) - return position -end - -local my_label_style = { - minimal_width = 450, - maximal_width = 450, - font_color = {r=1,g=1,b=1} -} - -local my_note_style = { - minimal_width = 450, - maximal_height = 10, - font = "default-small-semibold", - font_color = {r=1,g=0.5,b=0.5} -} - -local my_warning_style = { - minimal_width = 450, - maximal_width = 450, - font_color = {r=1,g=0.1,b=0.1} -} - -function ApplyStyle (guiIn, styleIn) - for k,v in pairs(styleIn) do - guiIn.style[k]=v - end -end - -function DisplaySpawnOptions(player) - player.gui.center.add{name = "spawn_opts", - type = "frame", - direction = "vertical", - caption="Spawn Options"} - local spawnGui = player.gui.center.spawn_opts - - spawnGui.style.maximal_width = 450 - spawnGui.style.maximal_height = 650 - - spawnGui.add{name = "warning_lbl1", type = "label", - caption="CHOOSE CAREFULLY - YOU ONLY GET ONE CUSTOM SPAWN POINT!"} - spawnGui.add{name = "warning_spacer", type = "label", - caption=" "} - ApplyStyle(spawnGui.warning_lbl1, my_warning_style) - ApplyStyle(spawnGui.warning_spacer, my_label_style) - - spawnGui.add{name = "normal_spawn", - type = "button", - caption="Default Spawn"} - spawnGui.add{name = "normal_spawn_lbl1", type = "label", - caption="This is the default spawn behavior of a vanilla game."} - spawnGui.add{name = "normal_spawn_lbl2", type = "label", - caption="You join the default team in the center of the map."} - spawnGui.add{name = "normal_spawn_spacer", type = "label", - caption=" "} - ApplyStyle(spawnGui.normal_spawn_lbl1, my_label_style) - ApplyStyle(spawnGui.normal_spawn_lbl2, my_label_style) - ApplyStyle(spawnGui.normal_spawn_spacer, my_label_style) - - spawnGui.add{name = "isolated_spawn", - type = "button", - caption="Isolated Spawn"} - spawnGui.add{name = "isolated_spawn_far", - type = "button", - caption="Isolated Spawn (Far Away)"} - spawnGui.add{name = "isolated_spawn_lbl1", type = "label", - caption="You are spawned in a new area, with starting resources."} - spawnGui.add{name = "isolated_spawn_lbl2", type = "label", - caption="You will still be part of the default team."} - spawnGui.add{name = "isolated_spawn_spacer", type = "label", - caption=" "} - ApplyStyle(spawnGui.isolated_spawn_lbl1, my_label_style) - ApplyStyle(spawnGui.isolated_spawn_lbl2, my_label_style) - ApplyStyle(spawnGui.isolated_spawn_spacer, my_label_style) - - - if (ENABLE_OTHER_TEAMS) then - spawnGui.add{name = "new_force", - type = "button", - caption="Separate Team"} - spawnGui.add{name = "new_force_far", - type = "button", - caption="Separate Team (Far Away)"} - spawnGui.add{name = "new_force_lbl1", type = "label", - caption="You are spawned in a new area, with starting resources."} - spawnGui.add{name = "new_force_lbl2", type = "label", - caption="You will be on your own team. (No shared vision or research with others.)"} - spawnGui.add{name = "new_force_lbl3", type = "label", - caption="Do not choose this option if you are new to the game!"} - spawnGui.add{name = "new_force_spacer", type = "label", - caption=" "} - ApplyStyle(spawnGui.new_force_lbl1, my_label_style) - ApplyStyle(spawnGui.new_force_lbl2, my_warning_style) - ApplyStyle(spawnGui.new_force_lbl3, my_warning_style) - ApplyStyle(spawnGui.new_force_spacer, my_label_style) - - - - spawnGui.add{name = "note_lbl1", type = "label", - caption="All members of a team share map vision and research."} - spawnGui.add{name = "note_lbl2", type = "label", - caption="To talk to someone on a different team, you need to use /s to shout."} - spawnGui.add{name = "note_lbl3", type = "label", - caption="All teams are neutral. This is still a cooperative PvE game... NOT PVP!"} - ApplyStyle(spawnGui.note_lbl1, my_note_style) - ApplyStyle(spawnGui.note_lbl2, my_note_style) - ApplyStyle(spawnGui.note_lbl3, my_note_style) - end - spawnGui.add{name = "note_lbl4", type = "label", - caption="Far away spawn is between 1000-6000 distance units away from the center of the map."} - spawnGui.add{name = "note_lbl5", type = "label", - caption="Isolated spawns are dangerous! You will have to fight to reach other players."} - spawnGui.add{name = "note_lbl6", type = "label", - caption="You can change your spawn options when you die."} - - ApplyStyle(spawnGui.note_lbl4, my_note_style) - ApplyStyle(spawnGui.note_lbl5, my_note_style) -end - -function DisplayRespawnContinueOption(player) - - player.gui.center.add{name = "respawn_continue_opts", - type = "frame", - direction = "vertical", - caption="Respawn Options"} - local respawnGui = player.gui.center.respawn_continue_opts - - respawnGui.style.maximal_width = 450 - respawnGui.style.maximal_height = 550 - - respawnGui.add{name = "respawn_continue", - type = "button", - caption="Continue"} - respawnGui.add{name = "respawn_continue_lbl1", type = "label", - caption="Continue at your current spawn location."} - respawnGui.add{name = "respawn_continue_spacer", type = "label", - caption=" "} - ApplyStyle(respawnGui.respawn_continue_lbl1, my_label_style) - ApplyStyle(respawnGui.respawn_continue_spacer, my_label_style) - - respawnGui.add{name = "respawn_change", - type = "button", - caption="Change Spawn"} - respawnGui.add{name = "respawn_change_lbl1", type = "label", - caption="Allow you to change your spawn and team."} - respawnGui.add{name = "respawn_change_spacer", type = "label", - caption=" "} - ApplyStyle(respawnGui.respawn_change_lbl1, my_label_style) - ApplyStyle(respawnGui.respawn_change_spacer, my_label_style) -end - - -function DisplayRespawnOptions(player) - - player.gui.center.add{name = "respawn_opts", - type = "frame", - direction = "vertical", - caption="Respawn Options"} - local respawnGui = player.gui.center.respawn_opts - - respawnGui.style.maximal_width = 450 - respawnGui.style.maximal_height = 750 - - -- Basically a cancel button to avoid choosing a different spawn. - respawnGui.add{name = "respawn_continue", - type = "button", - caption="Cancel"} - respawnGui.add{name = "respawn_continue_lbl1", type = "label", - caption="Continue with current spawn."} - respawnGui.add{name = "respawn_continue_spacer", type = "label", - caption=" "} - ApplyStyle(respawnGui.respawn_continue_lbl1, my_label_style) - ApplyStyle(respawnGui.respawn_continue_spacer, my_label_style) - - respawnGui.add{name = "respawn_mainforce", - type = "button", - caption="Use Default Spawn"} - respawnGui.add{name = "respawn_mainforce_lbl1", type = "label", - caption="This will join the default team."} - respawnGui.add{name = "respawn_mainforce_lbl2", type = "label", - caption="If you are on another team all your research will be lost!"} - respawnGui.add{name = "respawn_mainforce_lbl3", type = "label", - caption="You will spawn at the default spawn point in the center."} - respawnGui.add{name = "respawn_mainforce_spacer", type = "label", - caption=" "} - ApplyStyle(respawnGui.respawn_mainforce_lbl1, my_label_style) - ApplyStyle(respawnGui.respawn_mainforce_lbl2, my_warning_style) - ApplyStyle(respawnGui.respawn_mainforce_lbl3, my_label_style) - ApplyStyle(respawnGui.respawn_mainforce_spacer, my_label_style) - - - respawnGui.add{name = "respawn_custom_spawn", - type = "button", - caption="Custom Spawn"} - respawnGui.add{name = "respawn_custom_lbl1", type = "label", - caption="This will join the default team."} - respawnGui.add{name = "respawn_custom_lbl2", type = "label", - caption="If you are on another team all your research will be lost!"} - respawnGui.add{name = "respawn_custom_lbl3", type = "label", - caption="You will spawn at your previous custom spawn point."} - respawnGui.add{name = "respawn_custom_spacer", type = "label", - caption=" "} - ApplyStyle(respawnGui.respawn_custom_lbl1, my_label_style) - ApplyStyle(respawnGui.respawn_custom_lbl2, my_warning_style) - ApplyStyle(respawnGui.respawn_custom_lbl3, my_label_style) - ApplyStyle(respawnGui.respawn_custom_spacer, my_label_style) - - if (ENABLE_OTHER_TEAMS) then - respawnGui.add{name = "respawn_custom_team", - type = "button", - caption="Custom Team Spawn"} - respawnGui.add{name = "respawn_custom_team_lbl1", type = "label", - caption="This will join your own custom team."} - respawnGui.add{name = "respawn_custom_team_lbl2", type = "label", - caption="You will have your own map vision and research tree. Use /s to talk to others."} - respawnGui.add{name = "respawn_custom_team_lbl3", type = "label", - caption="You will spawn at your previous custom spawn point."} - respawnGui.add{name = "respawn_custom_team_spacer", type = "label", - caption=" "} - ApplyStyle(respawnGui.respawn_custom_team_lbl1, my_label_style) - ApplyStyle(respawnGui.respawn_custom_team_lbl2, my_warning_style) - ApplyStyle(respawnGui.respawn_custom_team_lbl3, my_label_style) - ApplyStyle(respawnGui.respawn_custom_team_spacer, my_label_style) - end - - if (global.enableRespawnSurprise == true) then - respawnGui.add{name = "respawn_surpise", - type = "button", - caption="Surprise me!"} - end - - respawnGui.add{name = "respawn_note1", type = "label", - caption="You cannot generate new custom spawn points."} - ApplyStyle(respawnGui.respawn_note1, my_note_style) -end - -function GenerateStartingResources(player) - local surface = player.surface - - -- Generate stone - local stonePos = {x=player.position.x-25, - y=player.position.y-31} - - -- Generate coal - local coalPos = {x=player.position.x-25, - y=player.position.y-16} - - -- Generate copper ore - local copperOrePos = {x=player.position.x-25, - y=player.position.y+0} - - -- Generate iron ore - local ironOrePos = {x=player.position.x-25, - y=player.position.y+15} - - -- Tree generation is taken care of in chunk generation - - -- Generate oil patches - surface.create_entity({name="crude-oil", amount=START_OIL_AMOUNT, - position={player.position.x-30, player.position.y-2}}) - surface.create_entity({name="crude-oil", amount=START_OIL_AMOUNT, - position={player.position.x-30, player.position.y+2}}) - - for y=0, 15 do - for x=0, 15 do - if ((x-7)^2 + (y - 7)^2 < 7^2) then - surface.create_entity({name="iron-ore", amount=START_IRON_AMOUNT, - position={ironOrePos.x+x, ironOrePos.y+y}}) - surface.create_entity({name="copper-ore", amount=START_COPPER_AMOUNT, - position={copperOrePos.x+x, copperOrePos.y+y}}) - surface.create_entity({name="stone", amount=START_STONE_AMOUNT, - position={stonePos.x+x, stonePos.y+y}}) - surface.create_entity({name="coal", amount=START_COAL_AMOUNT, - position={coalPos.x+x, coalPos.y+y}}) - end - end - end -end - -local function RemoveDecorationsArea(surface, area ) - for _, entity in pairs(surface.find_entities_filtered{area = area, type="decorative"}) do - entity.destroy() - end -end - -local function ClearNearbyEnemies(player) - local safeArea = {left_top= - {x=player.position.x-SAFE_AREA_TILE_DIST, - y=player.position.y-SAFE_AREA_TILE_DIST}, - right_bottom= - {x=player.position.x+SAFE_AREA_TILE_DIST, - y=player.position.y+SAFE_AREA_TILE_DIST}} - - for _, entity in pairs(player.surface.find_entities_filtered{area = safeArea, force = "enemy"}) do - entity.destroy() - end -end - - -local function DoesPlayerHaveCustomSpawn(player) - for name,spawnPos in pairs(global.playerSpawns) do - if (player.name == name) then - return true - end - end - return false -end - -local function DoesPlayerHaveActiveCustomSpawn(player) - if (DoesPlayerHaveCustomSpawn(player)) then - return global.activePlayerSpawns[player.name] - else - return false - end -end - -local function ActivatePlayerCustomSpawn(player, value) - for name,_ in pairs(global.playerSpawns) do - if (player.name == name) then - global.activePlayerSpawns[player.name] = value - break - end - end -end - -local function CreatePlayerCustomForce(player) - local newForce = nil - - -- Check if force already exists - if (game.forces[player.name] ~= nil) then - return game.forces[player.name] - - -- Create a new force using the player's name - elseif (TableLength(game.forces) < MAX_FORCES) then - newForce = game.create_force(player.name) - player.force = newForce - SetCeaseFireBetweenAllForces() - else - player.force = MAIN_FORCE - player.print("Sorry, no new teams can be created. You were assigned to the default team instead.") - end - - return newForce -end - -local function SendPlayerToNewSpawnAndCreateIt(player, spawn) - -- Send the player to that position - player.teleport(spawn) - GivePlayerStarterItems(player) - - -- If we get a valid spawn point, setup the area - if (spawn ~= {x=0,y=0}) then - GenerateStartingResources(player) - ClearNearbyEnemies(player) - end -end - -local function SendPlayerToActiveSpawn(player) - if (DoesPlayerHaveActiveCustomSpawn(player)) then - player.teleport(global.playerSpawns[player.name]) - else - player.teleport(game.forces[MAIN_FORCE].get_spawn_position("nauvis")) - end -end - -local function SendPlayerToRandomSpawn(player) - local numSpawns = TableLength(global.playerSpawns) - local rndSpawn = math.random(0,numSpawns) - local counter = 0 - - if (rndSpawn == 0) then - player.teleport(game.forces[MAIN_FORCE].get_spawn_position("nauvis")) - else - counter = counter + 1 - for name,spawnPos in pairs(global.playerSpawns) do - if (counter == rndSpawn) then - player.teleport(spawnPos) - break - end - counter = counter + 1 - end - end -end - - --- When a new player is created, present the spawn options --- Assign them to the main force so they can communicate with the team --- without shouting. -script.on_event(defines.events.on_player_created, function(event) - local player = game.players[event.player_index] - player.force = MAIN_FORCE - DisplaySpawnOptions(player) - player.print("Welcome to Oarc's server! Now with oil spots, better respawn menus and gravestone chests!") -end) - - --- Create the appropriate force & spawn when player selects their choice -script.on_event(defines.events.on_gui_click, function (event) - - local player = game.players[event.player_index] - local buttonClicked = event.element.name - - -- Only clear gui if a valid button was clicked!! - if ((buttonClicked == "normal_spawn") or - (buttonClicked == "isolated_spawn") or - (buttonClicked == "isolated_spawn_far") or - (buttonClicked == "new_force") or - (buttonClicked == "new_force_far") or - (buttonClicked == "respawn_continue") or - (buttonClicked == "respawn_change") or - (buttonClicked == "respawn_custom_team") or - (buttonClicked == "respawn_custom_spawn") or - (buttonClicked == "respawn_surpise") or - (buttonClicked == "respawn_mainforce")) then - - - - if (player.gui.center.spawn_opts ~= nil) then - player.gui.center.spawn_opts.destroy() - end - if (player.gui.center.respawn_opts ~= nil) then - player.gui.center.respawn_opts.destroy() - end - if (player.gui.center.respawn_continue_opts ~= nil) then - player.gui.center.respawn_continue_opts.destroy() - end - end - - -- In this option, the vanilla spawn is used and the player is - -- part of the main force. - if (buttonClicked == "normal_spawn") then - player.force = MAIN_FORCE - GivePlayerStarterItems(player) - SendBroadcastMsg(player.name .. " joined the main force!") - - -- In this option, the player gets a separate spawn point - -- but is still part of the main force. - elseif ((buttonClicked == "isolated_spawn") or (buttonClicked == "isolated_spawn_far")) then - player.force = MAIN_FORCE - - -- Create a new spawn point - local newSpawn = {} - if (buttonClicked == "isolated_spawn_far") then - newSpawn = CreateNewSpawnCoordinates(FAR_CHUNK_SPAWN_DIST, 20) - else - newSpawn = CreateNewSpawnCoordinates(MIN_CHUNK_SPAWN_DIST, 20) - end - global.playerSpawns[player.name] = newSpawn - global.activePlayerSpawns[player.name] = true - - SendPlayerToNewSpawnAndCreateIt(player, newSpawn) - if (buttonClicked == "isolated_spawn") then - SendBroadcastMsg(player.name .. " joined the main force from a distance!") - elseif (buttonClicked == "isolated_spawn_far") then - SendBroadcastMsg(player.name .. " joined the main force from a great distance!") - end - - -- In this option, the player is given a new force and a - -- separate spawn point - elseif ((buttonClicked == "new_force") or (buttonClicked == "new_force_far")) then - - -- Create a new force using the player's name - local newForce = CreatePlayerCustomForce(player) - - -- Create a new spawn point - local newSpawn = {} - if (buttonClicked == "new_force_far") then - newSpawn = CreateNewSpawnCoordinates(FAR_CHUNK_SPAWN_DIST, 20) - else - newSpawn = CreateNewSpawnCoordinates(MIN_CHUNK_SPAWN_DIST, 20) - end - global.playerSpawns[player.name] = newSpawn - global.activePlayerSpawns[player.name] = true - - -- Set the new spawn point - if (newForce ~= nil) then - newForce.set_spawn_position(newSpawn, "nauvis") - end - - SendPlayerToNewSpawnAndCreateIt(player, newSpawn) - SendBroadcastMsg(player.name .. " is going it alone!") - - -- Continue to respawn on your own team at that location - elseif (buttonClicked == "respawn_continue") then - GivePlayerItems(player) - - - -- If changing your spawn behavior - elseif (buttonClicked == "respawn_change") then - if (DoesPlayerHaveCustomSpawn(player)) then - DisplayRespawnOptions(player) - else - DisplaySpawnOptions(player) - end - - -- Respawn with the main force in the default location - elseif (buttonClicked == "respawn_mainforce") then - - -- Remove custom force if it exists - if (player.force.name ~= MAIN_FORCE) then - game.merge_forces(player.name, MAIN_FORCE) - end - - -- Deactivate the stored spawn point - ActivatePlayerCustomSpawn(player, false) - player.teleport(player.force.get_spawn_position("nauvis")) - GivePlayerStarterItems(player) - SendBroadcastMsg(player.name .. " is returning to base!") - - -- Respawn in your already generated custom spawn area on the main team - elseif (buttonClicked == "respawn_custom_spawn") then - - -- Remove custom force if it exists - if (player.force.name ~= MAIN_FORCE) then - game.merge_forces(player.name, MAIN_FORCE) - end - - -- Activate the stored spawn point - ActivatePlayerCustomSpawn(player, true) - SendPlayerToActiveSpawn(player) - GivePlayerStarterItems(player) - SendBroadcastMsg(player.name .. " is returning to their outpost!") - - -- Respawn in your already generated custom spawn area but on your own - -- force. This force is created new if it doesn't exist. - elseif (buttonClicked == "respawn_custom_team") then - - -- Create a new force using the player's name - local newForce = CreatePlayerCustomForce(player) - - -- Set the new spawn point - if (newForce ~= nil) then - newForce.set_spawn_position(global.playerSpawns[player.name], "nauvis") - end - - -- Activate the stored spawn point - ActivatePlayerCustomSpawn(player, true) - SendPlayerToActiveSpawn(player) - GivePlayerStarterItems(player) - SendBroadcastMsg(player.name .. " is returning to their outpost alone!") - - -- lol wut - elseif (buttonClicked == "respawn_surpise") then - - -- Remove custom force if it exists - if (player.force.name ~= MAIN_FORCE) then - game.merge_forces(player.name, MAIN_FORCE) - end - - -- Activate the stored spawn point - SendPlayerToRandomSpawn(player) - GivePlayerStarterItems(player) - SendBroadcastMsg(player.name .. " is surprised!") - - end -end) - --- local testFlag = false - --- Check if the player has a different spawn point than the default one --- Make sure to give the default starting items -script.on_event(defines.events.on_player_respawned, function(event) - local player = game.players[event.player_index] - - -- local testSpawn - -- if testFlag then - -- testSpawn = CreateNewSpawnCoordinates(FAR_CHUNK_SPAWN_DIST, 20) - -- else - -- testSpawn = CreateNewSpawnCoordinates(MIN_CHUNK_SPAWN_DIST, 20) - -- end - -- testFlag = not testFlag - -- player.teleport(testSpawn) - -- DebugPrint("Test Spawn: " .. testSpawn.x .. "," .. testSpawn.y) - - - -- If a player has an active spawn, use it. - if (DoesPlayerHaveActiveCustomSpawn(player)) then - player.teleport(global.playerSpawns[player.name]) - end - - -- Display the respawn continue option - DisplayRespawnContinueOption(player) -end) - - --- New spawn area tile generation and enemy clearing must go here -script.on_event(defines.events.on_chunk_generated, function(event) - local surface = event.surface - if surface.name ~= "nauvis" then return end - local chunkArea = event.area - - -- This handles chunk generation near player spawns - -- If it is near a player spawn, it does a few things like make the area - -- safe and provide a guaranteed area of land and water tiles. - for name,spawnPos in pairs(global.playerSpawns) do - - local landArea = {left_top= - {x=spawnPos.x-ENFORCE_LAND_AREA_TILE_DIST, - y=spawnPos.y-ENFORCE_LAND_AREA_TILE_DIST}, - right_bottom= - {x=spawnPos.x+ENFORCE_LAND_AREA_TILE_DIST, - y=spawnPos.y+ENFORCE_LAND_AREA_TILE_DIST}} - - local safeArea = {left_top= - {x=spawnPos.x-SAFE_AREA_TILE_DIST, - y=spawnPos.y-SAFE_AREA_TILE_DIST}, - right_bottom= - {x=spawnPos.x+SAFE_AREA_TILE_DIST, - y=spawnPos.y+SAFE_AREA_TILE_DIST}} - - local warningArea = {left_top= - {x=spawnPos.x-WARNING_AREA_TILE_DIST, - y=spawnPos.y-WARNING_AREA_TILE_DIST}, - right_bottom= - {x=spawnPos.x+WARNING_AREA_TILE_DIST, - y=spawnPos.y+WARNING_AREA_TILE_DIST}} - - local chunkAreaCenter = {x=chunkArea.left_top.x+(CHUNK_SIZE/2), - y=chunkArea.left_top.y+(CHUNK_SIZE/2)} - - - - -- Make chunks near a spawn safe by removing enemies - if CheckIfInArea(chunkAreaCenter,safeArea) then - for _, entity in pairs(surface.find_entities_filtered{area = chunkArea, force = "enemy"}) do - entity.destroy() - end - - -- Create a warning area with reduced enemies - elseif CheckIfInArea(chunkAreaCenter,warningArea) then - local counter = 0 - for _, entity in pairs(surface.find_entities_filtered{area = chunkArea, force = "enemy"}) do - if ((counter % WARN_AREA_REDUCTION_RATIO) ~= 0) then - entity.destroy() - end - counter = counter + 1 - end - - -- Remove all big and huge worms - for _, entity in pairs(surface.find_entities_filtered{area = chunkArea, name = "medium-worm-turret"}) do - entity.destroy() - end - for _, entity in pairs(surface.find_entities_filtered{area = chunkArea, name = "big-worm-turret"}) do - entity.destroy() - end - - end - - -- Fill in any water to make sure we have guaranteed land mass at the spawn point. - if CheckIfInArea(chunkAreaCenter,landArea) then - - -- remove trees in the immediate areas? - for key, entity in pairs(surface.find_entities_filtered({area=chunkArea, type= "tree"})) do - if ((spawnPos.x - entity.position.x)^2 + (spawnPos.y - entity.position.y)^2 < ENFORCE_LAND_AREA_TILE_DIST^2) then - entity.destroy() - end - end - - 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. - local distVar = math.floor((spawnPos.x - i)^2 + (spawnPos.y - j)^2) - - -- Fill in all unexpected water in a circle - if (distVar < ENFORCE_LAND_AREA_TILE_DIST_SQUARED) then - if (surface.get_tile(i,j).collides_with("water-tile")) then - table.insert(dirtTiles, {name = "grass", position ={i,j}}) - end - end - - -- Create a circle of trees around the spawn point. - if ((distVar < ENFORCE_LAND_AREA_TILE_DIST_SQUARED-200) and - (distVar > ENFORCE_LAND_AREA_TILE_DIST_SQUARED-260)) then - surface.create_entity({name="tree-01", amount=1, position={i, j}}) - end - end - end - - - surface.set_tiles(dirtTiles) - end - - -- Provide a guaranteed spot of water to use for power generation - if CheckIfInArea(spawnPos,chunkArea) then - local waterTiles = {{name = "water", position ={spawnPos.x+0,spawnPos.y-30}}, - {name = "water", position ={spawnPos.x+1,spawnPos.y-30}}, - {name = "water", position ={spawnPos.x+2,spawnPos.y-30}}, - {name = "water", position ={spawnPos.x+3,spawnPos.y-30}}, - {name = "water", position ={spawnPos.x+4,spawnPos.y-30}}, - {name = "water", position ={spawnPos.x+5,spawnPos.y-30}}, - {name = "water", position ={spawnPos.x+6,spawnPos.y-30}}, - {name = "water", position ={spawnPos.x+7,spawnPos.y-30}}, - {name = "water", position ={spawnPos.x+8,spawnPos.y-30}}} - -- DebugPrint("Setting water tiles in this chunk! " .. chunkArea.left_top.x .. "," .. chunkArea.left_top.y) - surface.set_tiles(waterTiles) - end - end - - -- Remove decor to save on file size - RemoveDecorationsArea(surface, chunkArea) -end) +require("event") +require("config") +require("rso_control") -- MUST LOAD THIS before other modifications to chunk generation +require("oarc_utils") +require("separate_spawns") +require("tag") +-- On init stuff script.on_init(function(event) - - -- Containes an array of all player spawns - -- A secondary array tracks whether the character will respawn there. - if (global.playerSpawns == nil) then - global.playerSpawns = {} - global.activePlayerSpawns = {} + if ENABLE_SEPARATE_SPAWNS then + -- For separate spawns stuff required on init + InitSpawnGlobalsAndForces() + + -- Adjust alien params + game.map_settings.enemy_evolution.time_factor=0 + game.map_settings.enemy_evolution.destroy_factor = game.map_settings.enemy_evolution.destroy_factor / ENEMY_DESTROY_FACTOR_DIVISOR + game.map_settings.enemy_evolution.pollution_factor = game.map_settings.enemy_evolution.pollution_factor / ENEMY_POLLUTION_FACTOR_DIVISOR + game.map_settings.enemy_expansion.enabled = ENEMY_EXPANSION end - -- Adjust alien params - game.map_settings.enemy_evolution.time_factor=0 - game.map_settings.enemy_evolution.destroy_factor = game.map_settings.enemy_evolution.destroy_factor / ENEMY_DESTROY_FACTOR_DIVISOR - game.map_settings.enemy_evolution.pollution_factor = game.map_settings.enemy_evolution.pollution_factor / ENEMY_POLLUTION_FACTOR_DIVISOR - game.map_settings.enemy_expansion.enabled = ENEMY_EXPANSION - - game.create_force(MAIN_FORCE) - game.forces[MAIN_FORCE].set_spawn_position(game.forces["player"].get_spawn_position("nauvis"), "nauvis") - SetCeaseFireBetweenAllForces() + if FRONTIER_ROCKET_SILO_MODE then + game.forces[MAIN_FORCE].chart(game.surfaces["nauvis"], {{SILO_POSITION.x-CHUNK_SIZE, SILO_POSITION.y-CHUNK_SIZE}, {SILO_POSITION.x+CHUNK_SIZE, SILO_POSITION.y+CHUNK_SIZE}}) + end end) - -- Freeplay rocket launch info +-- Slightly modified for my purposes script.on_event(defines.events.on_rocket_launched, function(event) local force = event.rocket.force if event.rocket.get_item_count("satellite") == 0 then @@ -940,66 +76,3 @@ script.on_event(defines.events.on_rocket_launched, function(event) end end end) - --- Return steel chest entity (or nil) -local function DropEmptySteelChest(player) - local pos = player.surface.find_non_colliding_position("steel-chest", player.position, 15, 1) - if not pos then - return nil - end - local grave = player.surface.create_entity{name="steel-chest", position=pos, force="neutral"} - return grave -end - --- My modified version of gravestone. Makes sure you get all items even if --- it requires multiple steel chests. -script.on_event(defines.events.on_player_died, function(event) - - local player = game.players[event.player_index] - - -- Create the 1st chest to fill - local grave = DropEmptySteelChest(player) - if (grave == nil) then - player.print("Not able to place a chest nearby! All items lost!") - return - end - - -- Init the inventroy space counter - local grave_inv = grave.get_inventory(defines.inventory.chest) - local count = 0 - - -- Loop through a players different inventories - -- Put it all into the chest - -- If the chest is full, create a new chest. - for i, id in ipairs{ - defines.inventory.player_armor, - defines.inventory.player_main, - defines.inventory.player_quickbar, - defines.inventory.player_guns, - defines.inventory.player_ammo, - defines.inventory.player_tools, - defines.inventory.player_trash} do - local inv = player.get_inventory(id) - for j = 1, #inv do - if inv[j].valid_for_read then - count = count + 1 - - grave_inv[count].set_stack(inv[j]) - - if (count == #grave_inv) then - count = 0 - grave = DropEmptySteelChest(player) - if (grave == nil) then - player.print("Not able to place a chest nearby! Some items lost!") - return - end - grave_inv = grave.get_inventory(defines.inventory.chest) - - end - end - end - end - - player.print("Successfully dropped your items into a chest! Go get them quick!") - -end) diff --git a/drand.lua b/drand.lua new file mode 100644 index 0000000..efd8b0f --- /dev/null +++ b/drand.lua @@ -0,0 +1,269 @@ +--[[------------------------------------ +RandomLua v0.3.1 +Pure Lua Pseudo-Random Numbers Generator +Under the MIT license. +copyright(c) 2011 linux-man +--]]------------------------------------ + +local _M = {} +local mod = math.fmod +local floor = math.floor +local abs = math.abs + +local function normalize(n) --keep numbers at (positive) 32 bits + return n % 0x80000000 +end + +local function bit_and(a, b) + local r = 0 + local m = 0 + for m = 0, 31 do + if (a % 2 == 1) and (b % 2 == 1) then r = r + 2^m end + if a % 2 ~= 0 then a = a - 1 end + if b % 2 ~= 0 then b = b - 1 end + a = a / 2 b = b / 2 + end + return normalize(r) +end + +local function bit_or(a, b) + local r = 0 + local m = 0 + for m = 0, 31 do + if (a % 2 == 1) or (b % 2 == 1) then r = r + 2^m end + if a % 2 ~= 0 then a = a - 1 end + if b % 2 ~= 0 then b = b - 1 end + a = a / 2 b = b / 2 + end + return normalize(r) +end + +local function bit_xor(a, b) + local r = 0 + local m = 0 + for m = 0, 31 do + if a % 2 ~= b % 2 then r = r + 2^m end + if a % 2 ~= 0 then a = a - 1 end + if b % 2 ~= 0 then b = b - 1 end + a = a / 2 b = b / 2 + end + return normalize(r) +end + +local function seed() + --return normalize(tonumber(tostring(os.time()):reverse())) + return normalize(os.time()) +end + +--Mersenne twister +mersenne_twister = {} +mersenne_twister.__index = mersenne_twister + +function mersenne_twister:randomseed(s) + if not s then s = seed() end + self.mt[0] = normalize(s) + for i = 1, 623 do + self.mt[i] = normalize(0x6c078965 * bit_xor(self.mt[i-1], floor(self.mt[i-1] / 0x40000000)) + i) + end +end + +function mersenne_twister:random(a, b) + local y + if self.index == 0 then + for i = 0, 623 do + --y = bit_or(floor(self.mt[i] / 0x80000000) * 0x80000000, self.mt[(i + 1) % 624] % 0x80000000) + y = self.mt[(i + 1) % 624] % 0x80000000 + self.mt[i] = bit_xor(self.mt[(i + 397) % 624], floor(y / 2)) + if y % 2 ~= 0 then self.mt[i] = bit_xor(self.mt[i], 0x9908b0df) end + end + end + y = self.mt[self.index] + y = bit_xor(y, floor(y / 0x800)) + y = bit_xor(y, bit_and(normalize(y * 0x80), 0x9d2c5680)) + y = bit_xor(y, bit_and(normalize(y * 0x8000), 0xefc60000)) + y = bit_xor(y, floor(y / 0x40000)) + self.index = (self.index + 1) % 624 + if not a then return y / 0x80000000 + elseif not b then + if a == 0 then return y + else return 1 + (y % a) + end + else + return a + (y % (b - a + 1)) + end +end + +function _M.twister(s) + local temp = {} + setmetatable(temp, mersenne_twister) + temp.mt = {} + temp.index = 0 + temp:randomseed(s) + return temp +end + +--Linear Congruential Generator +linear_congruential_generator = {} +linear_congruential_generator.__index = linear_congruential_generator + +function linear_congruential_generator:random(a, b) + local y = (self.a * self.x + self.c) % self.m + self.x = y + if not a then return y / 0x10000 + elseif not b then + if a == 0 then return y + else return 1 + (y % a) end + else + return a + (y % (b - a + 1)) + end +end + +function linear_congruential_generator:randomseed(s) + if not s then s = seed() end + self.x = normalize(s) +end + +function _M.lcg(s, r) + local temp = {} + setmetatable(temp, linear_congruential_generator) + temp.a, temp.c, temp.m = 1103515245, 12345, 0x10000 --from Ansi C + if r then + if r == 'nr' then temp.a, temp.c, temp.m = 1664525, 1013904223, 0x10000 --from Numerical Recipes. + elseif r == 'mvc' then temp.a, temp.c, temp.m = 214013, 2531011, 0x10000 end--from MVC + end + temp:randomseed(s) + return temp +end + +-- Multiply-with-carry +multiply_with_carry = {} +multiply_with_carry.__index = multiply_with_carry + +function multiply_with_carry:random(a, b) + local m = self.m + local t = self.a * self.x + self.c + local y = t % m + self.x = y + self.c = floor(t / m) + if not a then return y / 0x10000 + elseif not b then + if a == 0 then return y + else return 1 + (y % a) end + else + local diff = 0 + if a == b then return a end + if a < 0 then + diff = abs(a) + a = a + diff + b = b + diff + end + return a + (y % (b - a + 1)) - diff + end +end + +function multiply_with_carry:randomseed(s) + if not s then s = seed() end + self.c = self.ic + self.x = normalize(s) +end + +function _M.mwc(s, r) + local temp = {} + setmetatable(temp, multiply_with_carry) + temp.a, temp.c, temp.m = 1103515245, 12345, 0x10000 --from Ansi C + if r then + if r == 'nr' then temp.a, temp.c, temp.m = 1664525, 1013904223, 0x10000 --from Numerical Recipes. + elseif r == 'mvc' then temp.a, temp.c, temp.m = 214013, 2531011, 0x10000 end--from MVC + end + temp.ic = temp.c + temp:randomseed(s) + return temp +end + +function _M.mwvc(s) + return _M.mwc(s, 'mvc') +end + +local B = 0x10000 + +-- rough adaptation of Knuth float generator +function _M.krandom( seedobj, fVal1, fVal2 ) + local ma = seedobj.ma + local seed = seedobj.seed + local mj, mk + if seed < 0 or not ma then + ma = {} + seedobj.ma = ma + mj = normalize( seed ) + mj = mod( mj, B ) + ma[55] = mj + mk = 1 + for i = 1, 54 do + local ii = mod( 21 * i, 55 ) + ma[ii] = mk + mk = mj - mk + if mk < 0 then mk = mk + B end + mj = ma[ii] + end + for k = 1, 4 do + for i = 1, 55 do + ma[i] = ma[i] - ma[ 1 + mod( i + 30, 55) ] + if ma[i] < 0 then ma[i] = ma[i] + B end + end + end + seedobj.inext = 0 + seedobj.inextp = 31 + seedobj.seed = 1 + end -- if + local inext = seedobj.inext + local inextp = seedobj.inextp + inext = inext + 1 + if inext == 56 then inext = 1 end + seedobj.inext = inext + inextp = inextp + 1 + if inextp == 56 then inextp = 1 end + seedobj.inextp = inextp + mj = ma[ inext ] - ma[ inextp ] + if mj < 0 then mj = mj + B end + ma[ inext ] = mj + local temp_rand = mj / B + if fVal2 then + return floor( fVal1 + 0.5 + temp_rand * ( fVal2 - fVal1 ) ) + elseif fVal1 then + return floor( temp_rand * fVal1 ) + 1 + else + return temp_rand + end +end + +-- Sys rand +sys_rand = {} +sys_rand.__index = sys_rand +function sys_rand:random(a, b) + local diff = 0 + if a and b and a == b then math.random(); return a end + if a and b then + if a < 0 then + diff = abs(a) + a = a + diff + b = b + diff + end + return math.random(a, b) - diff + end + if a and a == 0 then return floor(math.random() * 0x10000) end + if a then return math.random(a) end + return math.random() +end + +function sys_rand:randomseed(s) + -- ignore + return +end + +function _M.sys_rand(s) + local temp = {} + setmetatable(temp, sys_rand) + return temp +end + +return _M \ No newline at end of file diff --git a/event.lua b/event.lua new file mode 100644 index 0000000..9c22e39 --- /dev/null +++ b/event.lua @@ -0,0 +1,123 @@ +--Event Capture +--A 3Ra Gaming revision, original from Factorio-Stdlib by Afforess +-- @module Event + +function fail_if_missing(var, msg) + if not var then + if msg then + error(msg, 3) + else + error("Missing value", 3) + end + end + return false +end + + +Event = { + _registry = {}, + core_events = { + init = -1, + load = -2, + configuration_changed = -3, + _register = function(id) + if id == Event.core_events.init then + script.on_init(function() + Event.dispatch({name = Event.core_events.init, tick = game.tick}) + end) + elseif id == Event.core_events.load then + script.on_load(function() + Event.dispatch({name = Event.core_events.load, tick = -1}) + end) + elseif id == Event.core_events.configuration_changed then + script.on_configuration_changed(function(data) + Event.dispatch({name = Event.core_events.configuration_changed, tick = game.tick, data = data}) + end) + end + end + } +} + +--- Registers a function for a given event +-- @param event or array containing events to register +-- @param handler Function to call when event is triggered +-- @return #Event +function Event.register(event, handler) + fail_if_missing(event, "missing event argument") + + if type(event) == "number" then + event = {event} + end + + for _, event_id in pairs(event) do + fail_if_missing(event_id, "missing event id") + if handler == nil then + Event._registry[event_id] = nil + script.on_event(event_id, nil) + else + if not Event._registry[event_id] then + Event._registry[event_id] = {} + + if event_id >= 0 then + script.on_event(event_id, Event.dispatch) + else + Event.core_events._register(event_id) + end + end + table.insert(Event._registry[event_id], handler) + end + end + return Event +end + +--- Calls the registerd handlers +-- @param event LuaEvent as created by game.raise_event +function Event.dispatch(event) + fail_if_missing(event, "missing event argument") + if Event._registry[event.name] then + for _, handler in pairs(Event._registry[event.name]) do + local metatbl = { __index = function(tbl, key) if key == '_handler' then return handler else return rawget(tbl, key) end end } + setmetatable(event, metatbl) + local success, err = pcall(handler, event) + if not success then + -- may be nil in on_load + game.print(err) + return + end + if err then + return + end + end + end +end + +--- Removes the handler from the event +-- @param event event or array containing events to remove the handler +-- @param handler to remove +-- @return #Event +function Event.remove(event, handler) + fail_if_missing(event, "missing event argument") + fail_if_missing(handler, "missing handler argument") + + if type(event) == "number" then + event = {event} + end + + for _, event_id in pairs(event) do + fail_if_missing(event_id, "missing event id") + if Event._registry[event_id] then + for i=#Event._registry[event_id], 1, -1 do + if Event._registry[event_id][i] == handler then + table.remove(Event._registry[event_id], i) + end + end + if #Event._registry[event_id] == 0 then + Event._registry[event_id] = nil + script.on_event(event_id, nil) + end + end + end + return Event +end + +return Event diff --git a/metaball.lua b/metaball.lua new file mode 100644 index 0000000..58ef918 --- /dev/null +++ b/metaball.lua @@ -0,0 +1,104 @@ +--[[-- +Metaball implementation for LUA by Dark +For bruteforce usage, nor efficient nor fast + +Force scales to from inf to 1 at R +--]]-- +local _M = {} +local sqrt = math.sqrt +local cos = math.cos +local sin = math.sin +local abs = math.abs +local zero_value = 0x80000000 + +--Classic ball +local MetaBall = {x=0, y=0, radius=0, goo=1, type="MetaBall"} +MetaBall.__index = MetaBall +_M.MetaBall=MetaBall + +function MetaBall:new(x, y, radius, goo) + goo = goo or 1 + return setmetatable({x=x, y=y, radius=radius, goo=goo}, MetaBall) +end + +function MetaBall:force(x, y) + --Calculate force at point x y + local force = sqrt( (x - self.x)^2 + (y - self.y)^2 ) + if force == 0 then return zero_value end + return (self.radius / force)^self.goo +end + +--Ellipse +local MetaEllipse = {x=0, y=0, radius=0, angle=0, x_scale=1, y_scale=1, type="MetaEllipse"} +MetaEllipse.__index = MetaEllipse +_M.MetaEllipse=MetaEllipse + +function MetaEllipse:new(x, y, radius, angle, x_scale, y_scale, goo) + angle = angle or 0 + x_scale = x_scale or 1 + y_scale = y_scale or 1 + goo = goo or 1 + cosa = cos(angle) + sina = sin(angle) + return setmetatable({x=x, y=y, radius=radius, angle=angle, x_scale=x_scale, y_scale=y_scale, goo=goo, cosa=cosa, sina=sina}, MetaEllipse) +end + +function MetaEllipse:force(x, y) + --Calculate force at point x y + local force = sqrt( (( (x - self.x)*self.cosa + (y - self.y)*self.sina )^2)/(self.x_scale) + + (( (y - self.y)*self.cosa - (x - self.x)*self.sina )^2)/(self.y_scale) ) + if force == 0 then return zero_value end + return (self.radius / force)^self.goo +end + +--SquareBalls +local MetaSquare = {x=0, y=0, radius=0, angle=0, x_scale=1, y_scale=1, type="MetaSquare"} +MetaSquare.__index = MetaSquare +_M.MetaSquare=MetaSquare + +function MetaSquare:new(x, y, radius, angle, x_scale, y_scale, goo) + angle = angle or 0 + x_scale = x_scale or 1 + y_scale = y_scale or 1 + goo = goo or 1 + cosa = cos(angle) + sina = sin(angle) + return setmetatable({x=x, y=y, radius=radius, angle=angle, x_scale=x_scale, y_scale=y_scale, goo=goo, cosa=cosa, sina=sina}, MetaSquare) +end + +function MetaSquare:force(x, y) + --Calculate force at point x y + local force = ( abs( (x - self.x)*self.cosa + (y - self.y)*self.sina )/self.x_scale + + abs( (y - self.y)*self.cosa - (x - self.x)*self.sina )/self.y_scale ) + if force == 0 then return zero_value end + return (self.radius / force)^self.goo +end + +--Donuts +local MetaDonut = {x=0, y=0, radius=0, angle=0, x_scale=1, y_scale=1, type="MetaDonut"} +MetaDonut.__index = MetaDonut +_M.MetaDonut=MetaDonut + +function MetaDonut:new(x, y, out_r, int_r, angle, x_scale, y_scale, goo) + angle = angle or 0 + x_scale = x_scale or 1 + y_scale = y_scale or 1 + goo = goo or 1 + cosa = cos(angle) + sina = sin(angle) + if int_r >= out_r then error("int_r >= out_r ("..int_r.." > "..out_r); return; end + local radius = out_r--(out_r - int_r)*0.5 + local radius2 = int_r--(radius2 + radius)*0.5 + return setmetatable({x=x, y=y, radius=radius, radius2=radius2, x_scale=x_scale, y_scale=y_scale, goo=goo, cosa=cosa, sina=sina}, MetaDonut) +end + +function MetaDonut:force(x, y) + --Calculate force at point x y + local force = abs(self.radius - sqrt( (( (x - self.x)*self.cosa + (y - self.y)*self.sina )^2)/(self.x_scale) + + (( (y - self.y)*self.cosa - (x - self.x)*self.sina )^2)/(self.y_scale) )) + if force == 0 then return zero_value end + return (self.radius2 / force)^self.goo + +end + +return _M \ No newline at end of file diff --git a/oarc_utils.lua b/oarc_utils.lua new file mode 100644 index 0000000..d0d063b --- /dev/null +++ b/oarc_utils.lua @@ -0,0 +1,472 @@ +-- oarc_utils.lua +-- +-- My general purpose utility functions for factorio +-- Also contains some constants and gui styles + + +-------------------------------------------------------------------------------- +-- Useful constants +-------------------------------------------------------------------------------- +CHUNK_SIZE = 32 +MAX_FORCES = 64 +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- +-- GUI Label Styles +-------------------------------------------------------------------------------- +my_label_style = { + minimal_width = 450, + maximal_width = 450, + font_color = {r=1,g=1,b=1} +} +my_note_style = { + minimal_width = 450, + maximal_height = 10, + font = "default-small-semibold", + font_color = {r=1,g=0.5,b=0.5} +} +my_warning_style = { + minimal_width = 450, + maximal_width = 450, + font_color = {r=1,g=0.1,b=0.1} +} + +-------------------------------------------------------------------------------- +-- +-------------------------------------------------------------------------------- + +-- Print debug only to me while testing. +-- Should remove this if you are hosting it yourself. +function DebugPrint(msg) + if ((game.players["Oarc"] ~= nil) and (global.oarcDebugEnabled)) then + game.players["Oarc"].print("DEBUG: " .. msg) + end +end + +-- Broadcast messages +function SendBroadcastMsg(msg) + for name,player in pairs(game.connected_players) do + player.print(msg) + end +end + +-- Simple function to get total number of items in table +function TableLength(T) + local count = 0 + for _ in pairs(T) do count = count + 1 end + return count +end + +-- Give player these default items. +function GivePlayerItems(player) + player.insert{name="pistol", count=1} + player.insert{name="firearm-magazine", count=10} +end + +-- Additional starter only items +function GivePlayerStarterItems(player) + GivePlayerItems(player) + player.insert{name="iron-plate", count=8} + player.insert{name="burner-mining-drill", count = 1} + player.insert{name="stone-furnace", count = 1} +end + +-- Check if given position is in area bounding box +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 + +-- Ceasefire +-- All forces are always neutral +function SetCeaseFireBetweenAllForces() + for name,team in pairs(game.forces) do + if name ~= "neutral" and name ~= "enemy" then + for x,y in pairs(game.forces) do + if x ~= "neutral" and x ~= "enemy" then + team.set_cease_fire(x,true) + end + end + end + end +end + +-- Undecorator +function RemoveDecorationsArea(surface, area ) + for _, entity in pairs(surface.find_entities_filtered{area = area, type="decorative"}) do + entity.destroy() + end +end + +-- Apply a style option to a GUI +function ApplyStyle (guiIn, styleIn) + for k,v in pairs(styleIn) do + guiIn.style[k]=v + end +end + +-- Get a random 1 or -1 +function RandomNegPos() + if (math.random(0,1) == 1) then + return 1 + else + return -1 + end +end + +-- Create a random direction vector to look in +function GetRandomVector() + local randVec = {x=0,y=0} + while ((randVec.x == 0) and (randVec.y == 0)) do + randVec.x = math.random(-3,3) + randVec.y = math.random(-3,3) + end + DebugPrint("direction: x=" .. randVec.x .. ", y=" .. randVec.y) + return randVec +end + +-- Check for ungenerated chunks around a specific chunk +-- +/- chunkDist in x and y directions +function IsChunkAreaUngenerated(chunkPos, chunkDist) + for x=-chunkDist, chunkDist do + for y=-chunkDist, chunkDist do + local checkPos = {x=chunkPos.x+x, + y=chunkPos.y+y} + if (game.surfaces["nauvis"].is_chunk_generated(checkPos)) then + return false + end + end + end + return true +end + +-- Clear out enemies around an area with a certain distance +function ClearNearbyEnemies(player, safeDist) + local safeArea = {left_top= + {x=player.position.x-safeDist, + y=player.position.y-safeDist}, + right_bottom= + {x=player.position.x+safeDist, + y=player.position.y+safeDist}} + + for _, entity in pairs(player.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 +function FindMapEdge(directionVec) + local position = {x=0,y=0} + local chunkPos = {x=0,y=0} + + -- Keep checking chunks in the direction of the vector + while(true) do + + -- Set some absolute limits. + if ((math.abs(chunkPos.x) > 1000) or (math.abs(chunkPos.y) > 1000)) then + break + + -- If chunk is already generated, keep looking + elseif (game.surfaces["nauvis"].is_chunk_generated(chunkPos)) then + chunkPos.x = chunkPos.x + directionVec.x + chunkPos.y = chunkPos.y + directionVec.y + + -- Found a possible ungenerated area + else + + chunkPos.x = chunkPos.x + directionVec.x + chunkPos.y = chunkPos.y + directionVec.y + + -- Check there are no generated chunks in a 10x10 area. + if IsChunkAreaUngenerated(chunkPos, 5) then + position.x = (chunkPos.x*CHUNK_SIZE) + (CHUNK_SIZE/2) + position.y = (chunkPos.y*CHUNK_SIZE) + (CHUNK_SIZE/2) + break + end + end + end + + DebugPrint("spawn: x=" .. position.x .. ", y=" .. position.y) + return position +end + +-- Find random coordinates within a given distance away +-- maxTries is the recursion limit basically. +function FindUngeneratedCoordinates(minDistChunks, maxDistChunks) + local position = {x=0,y=0} + local chunkPos = {x=0,y=0} + + local maxTries = 100 + local tryCounter = 0 + + local minDistSqr = minDistChunks^2 + local maxDistSqr = maxDistChunks^2 + + while(true) do + chunkPos.x = math.random(0,maxDistChunks) * RandomNegPos() + chunkPos.y = math.random(0,maxDistChunks) * RandomNegPos() + + local distSqrd = chunkPos.x^2 + chunkPos.y^2 + + -- Enforce a max number of tries + tryCounter = tryCounter + 1 + if (tryCounter > maxTries) then + break + + -- Check that the distance is within the min,max specified + elseif ((distSqrd < minDistSqr) or (distSqrd > maxDistSqr)) then + -- Keep searching! + + -- Check there are no generated chunks in a 10x10 area. + elseif IsChunkAreaUngenerated(chunkPos, 5) then + position.x = (chunkPos.x*CHUNK_SIZE) + (CHUNK_SIZE/2) + position.y = (chunkPos.y*CHUNK_SIZE) + (CHUNK_SIZE/2) + break -- SUCCESS + end + end + + DebugPrint("spawn: x=" .. position.x .. ", y=" .. position.y) + return position +end + +-- Return steel chest entity (or nil) +function DropEmptySteelChest(player) + local pos = player.surface.find_non_colliding_position("steel-chest", player.position, 15, 1) + if not pos then + return nil + end + local grave = player.surface.create_entity{name="steel-chest", position=pos, force="neutral"} + return grave +end + +-- Gravestone soft mod. With my own modifications/improvements. +function DropGravestoneChests(player) + + local grave + local count = 0 + + -- Use "game.player.cursorstack" to get items in player's hand. + + -- Loop through a players different inventories + -- Put it all into the chest + -- If the chest is full, create a new chest. + for i, id in ipairs{ + defines.inventory.player_armor, + defines.inventory.player_main, + defines.inventory.player_quickbar, + defines.inventory.player_guns, + defines.inventory.player_ammo, + defines.inventory.player_tools, + defines.inventory.player_trash} do + local inv = player.get_inventory(id) + if (not inv.is_empty()) then + for j = 1, #inv do + if inv[j].valid_for_read then + + -- Create a chest when counter is reset + if (count == 0) then + grave = DropEmptySteelChest(player) + if (grave == nil) then + player.print("Not able to place a chest nearby! Some items lost!") + return + end + grave_inv = grave.get_inventory(defines.inventory.chest) + end + count = count + 1 + + grave_inv[count].set_stack(inv[j]) + + -- Reset counter when chest is full + if (count == #grave_inv) then + count = 0 + end + end + end + end + end + + if (grave ~= nil) then + player.print("Successfully dropped your items into a chest! Go get them quick!") + end +end + + +-- Enforce a circle of land, also adds trees in a ring around the area. +function CreateCropCircle(surface, centerPos, chunkArea, tileRadius) + + local tileRadSqr = tileRadius^2 + + 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. + local distVar = math.floor((centerPos.x - i)^2 + (centerPos.y - j)^2) + + -- Fill in all unexpected water in a circle + if (distVar < tileRadSqr) then + if (surface.get_tile(i,j).collides_with("water-tile")) then + table.insert(dirtTiles, {name = "grass", position ={i,j}}) + end + end + + -- Create a circle of trees around the spawn point. + if ((distVar < tileRadSqr-200) and + (distVar > tileRadSqr-260)) then + surface.create_entity({name="tree-01", amount=1, position={i, j}}) + end + end + end + + + surface.set_tiles(dirtTiles) +end + +-- THIS DOES NOT WORK IN SCENARIOS! +-- function DisableVanillaResouresAndEnemies() + +-- local map_gen_ctrls = game.surfaces["nauvis"].map_gen_settings.autoplace_controls + +-- map_gen_ctrls["coal"].size = "none" +-- map_gen_ctrls["stone"].size = "none" +-- map_gen_ctrls["iron-ore"].size = "none" +-- map_gen_ctrls["copper-ore"].size = "none" +-- map_gen_ctrls["crude-oil"].size = "none" +-- map_gen_ctrls["enemy-base"].size = "none" +-- end + +-- Create a rocket silo +function CreateRocketSilo(surface, chunkArea) + if CheckIfInArea(SILO_POSITION, chunkArea) then + + -- Delete any entities beneat the silo? + for _, entity in pairs(surface.find_entities_filtered{area = {{SILO_POSITION.x-5, SILO_POSITION.y-6},{SILO_POSITION.x+6, SILO_POSITION.y+6}}}) do + entity.destroy() + end + + -- Set tiles below the silo + local tiles = {} + local i = 1 + for dx = -6,6 do + for dy = -7,6 do + tiles[i] = {name = "grass", position = {SILO_POSITION.x+dx, SILO_POSITION.y+dy}} + i=i+1 + end + end + surface.set_tiles(tiles, false) + tiles = {} + i = 1 + for dx = -5,5 do + for dy = -6,5 do + tiles[i] = {name = "concrete", position = {SILO_POSITION.x+dx, SILO_POSITION.y+dy}} + i=i+1 + end + end + surface.set_tiles(tiles, true) + + -- Create silo and assign to main force + local silo = surface.create_entity{name = "rocket-silo", position = {SILO_POSITION.x+0.5, SILO_POSITION.y}, force = MAIN_FORCE} + silo.destructible = false + silo.minable = false + end +end + +-- Remove rocket silo from recipes +function RemoveRocketSiloRecipe(event) + local recipes = event.research.force.recipes + if recipes["rocket-silo"] then + recipes["rocket-silo"].enabled = false + end +end + +-- Generates the rocket silo during chunk generation event +-- Includes a crop circle +function GenerateRocketSiloChunk(event) + local surface = event.surface + if surface.name ~= "nauvis" then return end + local chunkArea = event.area + + local chunkAreaCenter = {x=chunkArea.left_top.x+(CHUNK_SIZE/2), + y=chunkArea.left_top.y+(CHUNK_SIZE/2)} + local safeArea = {left_top= + {x=SILO_POSITION.x-150, + y=SILO_POSITION.y-150}, + right_bottom= + {x=SILO_POSITION.x+150, + y=SILO_POSITION.y+150}} + + + -- Clear enemies directly next to the rocket + if CheckIfInArea(chunkAreaCenter,safeArea) then + for _, entity in pairs(surface.find_entities_filtered{area = chunkArea, force = "enemy"}) do + entity.destroy() + end + end + + -- Create rocket silo + CreateRocketSilo(surface, chunkArea) + CreateCropCircle(surface, SILO_POSITION, chunkArea, ENFORCE_LAND_AREA_TILE_DIST) +end + +-- Display messages to a user everytime they join +function PlayerJoinedMessages(event) + local player = game.players[event.player_index] + player.print(WELCOME_MSG) + player.print(GAME_MODE_MSG) + player.print(MODULES_ENABLED) +end + +-- Create the gravestone chests for a player when they die +function CreateGravestoneChestsOnDeath(event) + DropGravestoneChests(game.players[event.player_index]) +end + +-- Remove decor to save on file size +function UndecorateOnChunkGenerate(event) + local surface = event.surface + local chunkArea = event.area + RemoveDecorationsArea(surface, chunkArea) +end + +-- Give player items on respawn +-- Intended to be the default behavior when not using separate spawns +function PlayerRespawnItems(event) + GivePlayerItems(game.players[event.player_index]) +end + +function PlayerSpawnItems(event) + GivePlayerStarterItems(game.players[event.player_index]) +end + +-------------------------------------------------------------------------------- +-- Register events +-- These must be placed after the functions that are referenced! +-------------------------------------------------------------------------------- + +Event.register(defines.events.on_player_joined_game, PlayerJoinedMessages) + +-- Rocket SIlo "frontier" mode +if FRONTIER_ROCKET_SILO_MODE then + Event.register(defines.events.on_chunk_generated, GenerateRocketSiloChunk) + Event.register(defines.events.on_research_finished, RemoveRocketSiloRecipe) +end + +if ENABLE_GRAVESTONE_CHESTS then + Event.register(defines.events.on_player_died, CreateGravestoneChestsOnDeath) +end + +if ENABLE_UNDECORATOR then + Event.register(defines.events.on_chunk_generated, UndecorateOnChunkGenerate) +end + +-- Separate teams mod does the starting items itself +if not ENABLE_SEPARATE_SPAWNS then + Event.register(defines.events.on_player_created, PlayerSpawnItems) + Event.register(defines.events.on_player_respawned, PlayerRespawnItems) +end diff --git a/rso_config.lua b/rso_config.lua new file mode 100644 index 0000000..37c8cef --- /dev/null +++ b/rso_config.lua @@ -0,0 +1,52 @@ + +debug_enabled = false +debug_items_enabled = false + +region_size = 7 -- alternative mean to control how further away resources would be, default - 256 tiles or 8 chunks + -- each region is region_size*region_size chunks + -- each chunk is 32*32 tiles + +use_donut_shapes = false -- setting this to false will remove donuts from possible resource layouts + +starting_area_size = 1 -- starting area in regions, safe from random nonsense + +absolute_resource_chance = 0.60 -- chance to spawn an resource in a region +starting_richness_mult = 1 -- multiply starting area richness for resources +global_richness_mult = 1 -- multiply richness for all resources except starting area +global_size_mult = 1 -- multiply size for all ores, doesn't affect starting area + +absolute_enemy_chance = 0.25 -- chance to spawn enemies per sector (can be more then one base if spawned) +enemy_base_size_multiplier = 1 -- all base sizes will be multiplied by this - larger number means bigger bases + +multi_resource_active = true -- global switch for multi resource chances +multi_resource_richness_factor = 0.60 -- any additional resource is multiplied by this value times resources-1 +multi_resource_size_factor = 0.90 +multi_resource_chance_diminish = 0.6 -- diminishing effect factor on multi_resource_chance + +min_amount=250 -- default value for minimum amount of resource in single pile + +richness_distance_factor=0.7 -- exponent for richness distance factor calculation +size_distance_factor=0.1 -- exponent for size distance factor calculation + +deterministic = true -- set to false to use system for all decisions math.random + +-- mode is no longer used by generation process - it autodetects endless resources +-- endless_resource_mode = false -- if true, the size of each resource is modified by the following modifier. Use with the endless resources mod. +endless_resource_mode_sizeModifier = 0.80 + +disableEnemyExpansion = false -- allows for disabling of in-game biter base building +use_RSO_biter_spawning = true -- enables spawning of biters controlled by RSO mod - less enemies around with more space between bases +use_vanilla_biter_spawning = false -- enables using of vanilla spawning + +biter_ratio_segment=1 --the ratio components determining how many biters to spitters will be spawned +spitter_ratio_segment=1 --eg. 1 and 1 -> equal number of biters and spitters, 10 and 1 -> 10 times as many biters to spitters + +useEnemiesInPeaceMod = false -- additional override for peace mod detection - when set to true it will spawn enemies normally, needs to have enemies enabled in peace mod + +ignoreMapGenSettings = true -- stops the default behaviour of reading map gen settings + +fluidResourcesFactor = 20 -- temporary factor for calculation of resource %-ages for fluids + +useResourceCollisionDetection = true -- enables avoidace calculations to reduce ores overlaping of each other +resourceCollisionDetectionRatio = 0.8 -- at least this much of ore field needs to be placable to spawn it +resourceCollisionFieldSkip = true -- determines if ore field should be skipped completely if placement based on ratio failed diff --git a/rso_control.lua b/rso_control.lua new file mode 100644 index 0000000..7885798 --- /dev/null +++ b/rso_control.lua @@ -0,0 +1,1364 @@ +require "event" +require "rso_config" +require "util" +require "rso_resource_config" + +local MB=require "metaball" +local drand = require 'drand' +local rng = drand.mwvc +if not deterministic then rng = drand.sys_rand end + +-- math shortcuts +local floor = math.floor +local abs = math.abs +local cos = math.cos +local sin = math.sin +local pi = math.pi +local max = math.max + +local function round(value) + return math.floor(value + 0.5) +end + +local function debug(str) + if debug_enabled then + game.players[1].print(str) + end +end + +-- constants +local CHUNK_SIZE = 32 +local REGION_TILE_SIZE = CHUNK_SIZE*region_size +local MIN_BALL_DISTANCE = CHUNK_SIZE/6 +local P_BALL_SIZE_FACTOR = 0.7 +local N_BALL_SIZE_FACTOR = 0.95 +local NEGATIVE_MODIFICATOR = 123456 + +local meta_shapes = nil + +if use_donut_shapes then + meta_shapes = {MB.MetaEllipse, MB.MetaSquare, MB.MetaDonut} +else + meta_shapes = {MB.MetaEllipse, MB.MetaSquare} +end + +-- local globals +local index_is_built = false +local max_allotment = 0 +local rgen = nil +local distance = util.distance +local spawner_probability_edge = 0 -- below this value a biter spawner, above/equal this value a spitter spawner +local invalidResources = {} +local config = nil +local configIndexed = nil + +-- map gen settings mapping + +local startingAreaMultiplier = +{ + none = 0, + ["very-low"] = 0.25, + low = 0.5, + normal = 1, + high = 1.5, + ["very-high"] = 2, +} + +local frequencyAllotmentMultiplier = +{ + ["very-low"] = 0.5, + low = 0.75, + normal = 1, + high = 1.5, + ["very-high"] = 2, +} + +local sizeMultiplier = +{ + none = 0, + ["very-low"] = 0.5, + low = 0.75, + normal = 1, + high = 1.25, + ["very-high"] = 1.5, +} + +local richnessMultiplier = +{ + ["very-low"] = 0.125, + low = 0.25, + normal = 1, + high = 2, + ["very-high"] = 4, +} + +local entityFrequencyMultiplier = +{ + ["very-low"] = 0.25, + low = 0.5, + normal = 1, + high = 2, + ["very-high"] = 4, +} + +local entitySizeMultiplier = +{ + none = 0, + ["very-low"] = 0.5, + low = 0.75, + normal = 1, + high = 2, + ["very-high"] = 4, +} + +--[[ HELPER METHODS ]]-- + +local function normalize(n) --keep numbers at (positive) 32 bits + return floor(n) % 0x80000000 +end + +local function bearing(origin, dest) + -- finds relative angle + local xd = dest.x - origin.x + local yd = dest.y - origin.y + return math.atan2(xd, yd); +end + +local function str2num(s) + local num = 0 + for i=1,s:len() do + num=num + (s:byte(i) - 33)*i + end + return num +end + +local function mult_for_pos(pos) + local num = 0 + local x = pos.x + local y = pos.y + + if x == 0 then x = 0.5 end + if y == 0 then y = 0.5 end + if x < 0 then + x = abs(x) + NEGATIVE_MODIFICATOR + end + if y < 0 then + y = abs(y) + NEGATIVE_MODIFICATOR + end + + return drand.lcg(y, 'mvc'):random(0)*drand.lcg(x, 'nr'):random(0) +end + +local function rng_for_reg_pos(pos) + local rgen = rng(normalize(global.seed*mult_for_pos(pos))) + rgen:random() + rgen:random() + rgen:random() + return rgen +end + +local function rng_restricted_angle(restrictions) + local rng = rgen:random() + local x_scale, y_scale + local deformX = rgen:random() * 2 - 1 + local deformY = rgen:random() * 2 - 1 + + if restrictions=='xy' then + y_scale=1.0 + deformY*0.5 + x_scale=1.0 + deformX*0.5 + angle = rng*pi*2 + elseif restrictions=='x' then + y_scale=1.0 + deformY*0.6 + x_scale=1.0 + deformX*0.6 + angle = rng*pi/2 - pi/4 + elseif restrictions=='y' then + y_scale=1.0 + deformY*0.6 + x_scale=1.0 + deformX*0.6 + angle = rng*pi/2 + pi/2 + else + y_scale=1.0 + deformY*0.3 + x_scale=1.0 + deformX*0.3 + angle = rng*pi*2 + end + + return angle, x_scale, y_scale +end + +local function vary_by_percentage(x, p) + return x + (0.5 - rgen:random())*2*x*p +end + + +local function remove_trees(surface, x, y, x_size, y_size ) + local bb={{x - x_size, y - y_size}, {x + x_size, y + y_size}} + for _, entity in pairs(surface.find_entities_filtered{area = bb, type="tree"}) do + if entity.valid then + entity.destroy() + end + end +end + +local function removeDecorations(surface, x, y, width, height ) + local bb={{x, y}, {x + width, y + height}} + for _, entity in pairs(surface.find_entities_filtered{area = bb, type="decorative"}) do + if entity.valid then + entity.destroy() + end + end +end + +local function find_intersection(surface, x, y) + -- try to get position in between of valid chunks by probing map + -- this may breaks determinism of generation, but so far it returned on first if + local gt = surface.get_tile + local restriction = '' + if gt(x + CHUNK_SIZE*2, y + CHUNK_SIZE*2).valid and gt(x - CHUNK_SIZE*2, y - CHUNK_SIZE*2).valid and gt(x + CHUNK_SIZE*2, y - CHUNK_SIZE*2).valid and gt(x - CHUNK_SIZE*2, y + CHUNK_SIZE*2).valid then + restriction = 'xy' + elseif gt(x + CHUNK_SIZE*2, y + CHUNK_SIZE*2).valid and gt(x + CHUNK_SIZE*2, y).valid and gt(x, y + CHUNK_SIZE*2).valid then + x=x + CHUNK_SIZE/2 + y=y + CHUNK_SIZE/2 + restriction = 'xy' + elseif gt(x + CHUNK_SIZE*2, y - CHUNK_SIZE*2).valid and gt(x + CHUNK_SIZE*2, y).valid and gt(x, y - CHUNK_SIZE*2).valid then + x=x + CHUNK_SIZE/2 + y=y - CHUNK_SIZE/2 + restriction = 'xy' + elseif gt(x - CHUNK_SIZE*2, y + CHUNK_SIZE*2).valid and gt(x - CHUNK_SIZE*2, y).valid and gt(x, y + CHUNK_SIZE*2).valid then + x=x - CHUNK_SIZE/2 + y=y + CHUNK_SIZE/2 + restriction = 'xy' + elseif gt(x - CHUNK_SIZE*2, y - CHUNK_SIZE*2).valid and gt(x - CHUNK_SIZE*2, y).valid and gt(x, y - CHUNK_SIZE*2).valid then + x=x - CHUNK_SIZE/2 + y=y - CHUNK_SIZE/2 + restriction = 'xy' + elseif gt(x + CHUNK_SIZE*2, y).valid then + x=x + CHUNK_SIZE/2 + restriction = 'x' + elseif gt(x - CHUNK_SIZE*2, y).valid then + x=x - CHUNK_SIZE/2 + restriction = 'x' + elseif gt(x, y + CHUNK_SIZE*2).valid then + y=y + CHUNK_SIZE/2 + restriction = 'y' + elseif gt(x, y - CHUNK_SIZE*2).valid then + y=y - CHUNK_SIZE/2 + restriction = 'y' + end + return x, y, restriction +end + +local function find_random_chunk(r_x, r_y) + local offset_x=rgen:random(region_size)-1 + local offset_y=rgen:random(region_size)-1 + local c_x=r_x*REGION_TILE_SIZE + offset_x*CHUNK_SIZE + local c_y=r_y*REGION_TILE_SIZE + offset_y*CHUNK_SIZE + return c_x, c_y +end + +local function is_same_region(c_x1, c_y1, c_x2, c_y2) + if not floor(c_x1/REGION_TILE_SIZE) == floor(c_x2/REGION_TILE_SIZE) then + return false + end + if not floor(c_y1/REGION_TILE_SIZE) == floor(c_y2/REGION_TILE_SIZE) then + return false + end + return true +end + +local function find_random_neighbour_chunk(ocx, ocy) + -- somewhat bruteforce and unoptimized + local x_dir = rgen:random(-1,1) + local y_dir = rgen:random(-1,1) + local ncx = ocx + x_dir*CHUNK_SIZE + local ncy = ocy + y_dir*CHUNK_SIZE + if is_same_region(ncx, ncy, ocx, ocy) then + return ncx, ncy + end + + ncx = ocx - x_dir*CHUNK_SIZE + ncy = ocy - y_dir*CHUNK_SIZE + if is_same_region(ncx, ncy, ocx, ocy) then + return ncx, ncy + end + + ncx = ocx - x_dir*CHUNK_SIZE + if is_same_region(ncx, ocy, ocx, ocy) then + return ncx, ocy + end + + ncy = ocy - y_dir*CHUNK_SIZE + if is_same_region(ocx, ncy, ocx, ocy) then + return ocx, ncy + end + + return ocx, ocy +end + +local function isInStartingArea( regionX, regionY ) + + for idx, pos in pairs( global.startingAreas ) do + + local adjustedX = regionX - pos.x / REGION_TILE_SIZE + local adjustedY = regionY - pos.y / REGION_TILE_SIZE + if ((adjustedX * adjustedX + adjustedY * adjustedY) <= starting_area_size * starting_area_size) then + return true + end + end + + return false +end + +-- modifies the resource size - only used in endless_resource_mode +local function modify_resource_size(resourceName, resourceSize, startingArea) + + if not startingArea then + resourceSize = math.ceil(resourceSize * global_size_mult) + end + + resourceEntity = game.entity_prototypes[resourceName] + if resourceEntity and resourceEntity.infinite_resource then + + newResourceSize = resourceSize * endless_resource_mode_sizeModifier + + -- make sure it's still an integer + newResourceSize = math.ceil(newResourceSize) + -- make sure it's not 0 + if newResourceSize == 0 then newResourceSize = 1 end + return newResourceSize + else + return resourceSize + end +end + +--[[ SPAWN METHODS ]]-- + +local locationOrder = +{ + { x = 0, y = 0 }, + { x = -1, y = 0 }, + { x = 1, y = 0 }, + { x = 0, y = -1 }, + { x = 0, y = 1 }, + { x = -1, y = -1 }, + { x = 1, y = -1 }, + { x = -1, y = 1 }, + { x = 1, y = 1 } +} + +--[[ entity-field ]]-- +local function spawn_resource_ore(surface, rname, pos, size, richness, startingArea, restrictions) + -- blob generator, centered at pos, size controls blob diameter + restrictions = restrictions or '' + debug("Entering spawn_resource_ore "..rname.." at:"..pos.x..","..pos.y.." size:"..size.." richness:"..richness.." isStart:"..tostring(startingArea).." restrictions:"..restrictions) + + size = modify_resource_size(rname, size, startingArea) + local radius = size / 2 -- to radius + + local p_balls={} + local n_balls={} + local MIN_BALL_DISTANCE = math.min(MIN_BALL_DISTANCE, radius/2) + + local maxPradius = 0 + local outside = { xmin = 1e10, xmax = -1e10, ymin = 1e10, ymax = -1e10 } + local inside = { xmin = 1e10, xmax = -1e10, ymin = 1e10, ymax = -1e10 } + + local function adjustRadius(radius, scaleX, scaleY, up) + return radius + end + + local function updateRect(rect, x, y, radius) + rect.xmin = math.min(rect.xmin, x - radius) + rect.xmax = math.max(rect.xmax, x + radius) + rect.ymin = math.min(rect.ymin, y - radius) + rect.ymax = math.max(rect.ymax, y + radius) + end + + local function updateRects(x, y, radius, scaleX, scaleY) + local adjustedRadius = adjustRadius(radius, scaleX, scaleY, true) + local radiusMax = adjustedRadius * 3 -- arbitrary multiplier - needs to be big enough to not cut any metaballs + updateRect(outside, x, y, radiusMax) + updateRect(inside, x, y, adjustedRadius) + end + + local function generate_p_ball() + local angle, x_scale, y_scale, x, y, b_radius, shape + angle, x_scale, y_scale=rng_restricted_angle(restrictions) + local dev = radius / 2 + rgen:random() * radius / 4--math.min(CHUNK_SIZE/3, radius*1.5) + local dev_x, dev_y = pos.x, pos.y + x = rgen:random(-dev, dev)+dev_x + y = rgen:random(-dev, dev)+dev_y + if p_balls[#p_balls] and distance(p_balls[#p_balls], {x=x, y=y}) < MIN_BALL_DISTANCE then + local new_angle = bearing(p_balls[#p_balls], {x=x, y=y}) + debug("Move ball old xy @ "..x..","..y) + x=(cos(new_angle)*MIN_BALL_DISTANCE) + x + y=(sin(new_angle)*MIN_BALL_DISTANCE) + y + debug("Move ball new xy @ "..x..","..y) + end + + if #p_balls == 0 then + b_radius = ( 3 * radius / 4 + rgen:random() * radius / 4) -- * (P_BALL_SIZE_FACTOR^#p_balls) + else + b_radius = ( radius / 4 + rgen:random() * radius / 2) -- * (P_BALL_SIZE_FACTOR^#p_balls) + end + + + if #p_balls > 0 then + local tempRect = table.deepcopy(inside) + updateRect(tempRect, x, y, adjustRadius(b_radius, x_scale, y_scale)) + local rectSize = math.max(tempRect.xmax - tempRect.xmin, tempRect.ymax - tempRect.ymin) + local targetSize = size * 1.25 + debug("Rect size "..rectSize.." targetSize "..targetSize) + if rectSize > targetSize then + local widthLeft = (targetSize - (inside.xmax - inside.xmin)) + local heightLeft = (targetSize - (inside.ymax - inside.ymin)) + local widthMod = math.min(x - inside.xmin, inside.xmax - x) + local heightMod = math.min(y - inside.ymin, inside.ymax - y) + local radiusBackup = b_radius + b_radius = math.min(widthLeft + widthMod, heightLeft + heightMod) + b_radius = adjustRadius(b_radius, x_scale, y_scale, false) + debug("Reduced ball radius from "..radiusBackup.." to "..b_radius.." widthLeft:"..widthLeft.." heightLeft:"..heightLeft.." widthMod:"..widthMod.." heightMod:"..heightMod) + end + end + + if b_radius < 2 and #p_balls == 0 then + b_radius = 2 + end + + if b_radius > 0 then + + maxPradius = math.max(maxPradius, b_radius) + shape = meta_shapes[rgen:random(1,#meta_shapes)] + local radiusText = "" + if shape.type == "MetaDonut" then + local inRadius = b_radius / 4 + b_radius / 2 * rgen:random() + radiusText = " inRadius:"..inRadius + p_balls[#p_balls+1] = shape:new(x, y, b_radius, inRadius, angle, x_scale, y_scale, 1.1) + else + p_balls[#p_balls+1] = shape:new(x, y, b_radius, angle, x_scale, y_scale, 1.1) + end + updateRects(x, y, b_radius, x_scale, y_scale) + + debug("P+Ball "..shape.type.." @ "..x..","..y.." radius: "..b_radius..radiusText.." angle: "..math.deg(angle).." scale: "..x_scale..", "..y_scale) + end + end + + local function generate_n_ball(i) + local angle, x_scale, y_scale, x, y, b_radius, shape + angle, x_scale, y_scale=rng_restricted_angle('xy') + if p_balls[i] then + local new_angle = p_balls[i].angle + pi*rgen:random(0,1) + (rgen:random()-0.5)*pi/2 + local dist = p_balls[i].radius + x=(cos(new_angle)*dist) + p_balls[i].x + y=(sin(new_angle)*dist) + p_balls[i].y + angle = p_balls[i].angle + pi/2 + (rgen:random()-0.5)*pi*2/3 + else + x = rgen:random(-radius, radius)+pos.x + y = rgen:random(-radius, radius)+pos.y + end + + if p_balls[i] then + b_radius = (p_balls[i].radius / 4 + rgen:random() * p_balls[i].radius / 2) -- * (N_BALL_SIZE_FACTOR^#n_balls) + else + b_radius = (radius / 4 + rgen:random() * radius / 2) -- * (N_BALL_SIZE_FACTOR^#n_balls) + end + + if b_radius < 1 then + b_radius = 1 + end + + shape = meta_shapes[rgen:random(1,#meta_shapes)] + local radiusText = "" + if shape.type == "MetaDonut" then + local inRadius = b_radius / 4 + b_radius / 2 * rgen:random() + radiusText = " inRadius:"..inRadius + n_balls[#n_balls+1] = shape:new(x, y, b_radius, inRadius, angle, x_scale, y_scale, 1.15) + else + n_balls[#n_balls+1] = shape:new(x, y, b_radius, angle, x_scale, y_scale, 1.15) + end + -- updateRects(x, y, b_radius, x_scale, y_scale) -- should not be needed here - only positive ball can generate ore + debug("N-Ball "..shape.type.." @ "..x..","..y.." radius: "..b_radius..radiusText.." angle: "..math.deg(angle).." scale: "..x_scale..", "..y_scale) + end + + local function calculate_force(x,y) + local p_force = 0 + local n_force = 0 + for _,ball in ipairs(p_balls) do + p_force = p_force + ball:force(x,y) + end + for _,ball in ipairs(n_balls) do + n_force = n_force + ball:force(x,y) + end + local totalForce = 0 + if p_force > n_force then + totalForce = 1 - 1/(p_force - n_force) + end + --debug("Force at "..x..","..y.." p:"..p_force.." n:"..n_force.." result:"..totalForce) + --return (1 - 1/p_force) - n_force + return totalForce + end + + local max_p_balls = 2 + local min_amount = config[rname].min_amount or min_amount + if restrictions == 'xy' then + max_p_balls = 3 + end + + radius = math.min(radius, 2*CHUNK_SIZE) + + local force + -- generate blobs + for i=1,max_p_balls do + generate_p_ball() + end + + for i=1,rgen:random(1, #p_balls) do + generate_n_ball(i) + end + + + local _total = 0 + local oreLocations = {} + local forceTotal = 0 + + -- fill the map + for y=outside.ymin, outside.ymax do + + for x=outside.xmin, outside.xmax do + force = calculate_force(x, y) + if force > 0 then + oreLocations[#oreLocations + 1] = {x = x, y = y, force = force, valid = false} + forceTotal = forceTotal + force + end + end + end + + local validCount, resOffsetX, resOffsetY, ratio + + for _,locationOffset in ipairs(locationOrder) do + validCount = 0 + resOffsetX = locationOffset.x * CHUNK_SIZE + resOffsetY = locationOffset.y * CHUNK_SIZE + + for _, location in ipairs(oreLocations) do + + local newX = location.x + resOffsetX + local newY = location.y + resOffsetY + location.valid = false + if surface.can_place_entity{name = rname, position = {x = newX,y = newY}} then + location.valid = true + validCount = validCount + 1 + end + end + + ratio = 0 + + if validCount > 0 then + ratio = validCount / #oreLocations + end + + debug("Valid ratio ".. ratio) + + if not useResourceCollisionDetection then + break + end + + if ratio > resourceCollisionDetectionRatio then + break + elseif resourceCollisionFieldSkip then -- in case no valid ratio was found we skip the field completely + validCount = 0 + end + end + + if validCount > 0 then + local rectSize = ((inside.xmax - inside.xmin) + (inside.ymax - inside.ymin)) / 2 + + local sizeMultiplier = rectSize ^ 0.6 + local minSize = richness * 5 * sizeMultiplier + local maxSize = richness * 10 * sizeMultiplier + local approxDepositSize = rgen:random(minSize, maxSize) + + approxDepositSize = approxDepositSize - validCount * min_amount + + if approxDepositSize < 0 then + approxDepositSize = 100 * validCount + end + + local forceFactor = approxDepositSize / forceTotal + + -- don't create very dense resources in starting area - another field will be generated + if startingArea and forceFactor > 4000 then + forceFactor = rgen:random(3000, 4000) + end + + debug( "Force total:"..forceTotal.." sizeMin:"..minSize.." sizeMax:"..maxSize.." factor:"..forceFactor.." location#:"..validCount.." rectSize:"..rectSize.." sizeMultiplier:"..sizeMultiplier) + local richnessMultiplier = global_richness_mult + + if startingArea then + richnessMultiplier = starting_richness_mult + end + + -- infinite ore handling for Angels Ores mod + local infiniteOrePresent = false + local infiniteOreName = "infinite-"..rname + local minimumInfiniteOreAmount = nil + local spawnName = rname + + if game.entity_prototypes[infiniteOreName] then + infiniteOrePresent = true + minimumInfiniteOreAmount = game.entity_prototypes[infiniteOreName].minimum_resource_amount + end + + if startingArea and not infiniteResourceInStartArea then + infiniteOrePresent = false + end + + for _,location in ipairs(oreLocations) do + if location.valid then + + local amount = floor(( forceFactor * location.force + min_amount ) * richnessMultiplier) + + if amount > 1e9 then + amount = 1e9 + end + + _total = _total + amount + + spawnName = rname + if infiniteOrePresent and location.force > infiniteResourceSpawnThreshold then + spawnName = infiniteOreName + if minimumInfiniteOreAmount and amount < minimumInfiniteOreAmount then + amount = minimumInfiniteOreAmount + end + end + + if amount > 0 then + surface.create_entity{name = spawnName, + position = {location.x + resOffsetX,location.y + resOffsetY}, + force = game.forces.neutral, + amount = amount} + end + end + end + + end + + if debug_enabled then + debug("Total amount: ".._total) + debug("Leaving spawn_resource_ore") + end + return _total +end + +--[[ entity-liquid ]]-- +local function spawn_resource_liquid(surface, rname, pos, size, richness, startingArea, restrictions) + restrictions = restrictions or '' + debug("Entering spawn_resource_liquid "..rname.." "..pos.x..","..pos.y.." "..size.." "..richness.." "..tostring(startingArea).." "..restrictions) + local _total = 0 + local max_radius = rgen:random()*CHUNK_SIZE/2 + CHUNK_SIZE + + richness = ( 0.75 + rgen:random() / 2 ) * richness * size + + resourceEntity = game.entity_prototypes[rname] + + + local total_share = 0 + local avg_share = 1/size + local angle = rgen:random()*pi*2 + local saved = 0 + while total_share < 1 do + local new_share = vary_by_percentage(avg_share, 0.25) + if new_share + total_share > 1 then + new_share = 1 - total_share + end + total_share = new_share + total_share + if new_share < avg_share/10 then + -- too small + break + end + local amount = floor(richness*new_share) + saved + + local richnessMultiplier = global_richness_mult + + if startingArea then + richnessMultiplier = starting_richness_mult + end + + --if amount >= game.entity_prototypes[rname].minimum then + if amount >= config[rname].minimum_amount then + saved = 0 + for try=1,5 do + local dist = rgen:random()*(max_radius - max_radius*0.1) + angle = angle + pi/4 + rgen:random()*pi/2 + local x, y = pos.x + cos(angle)*dist, pos.y + sin(angle)*dist + if surface.can_place_entity{name = rname, position = {x,y}} then + debug("@ "..x..","..y.." amount: "..amount.." new_share: "..new_share.." try: "..try) + amount = floor(amount * richnessMultiplier) + + if amount > 1e9 then + amount = 1e9 + end + + _total = _total + amount + + if amount > 0 then + surface.create_entity{name = rname, + position = {x,y}, + force = game.forces.neutral, + amount = amount, + direction = rgen:random(4)} + end + break + elseif not startingArea then -- we don't want to make ultra rich nodes in starting area - failing to make them will add second spawn in different location + entities = surface.find_entities_filtered{area = {{x-2.75, y-2.75}, {x+2.75, y+2.75}}, name=rname} + if entities and #entities > 0 then + _total = _total + amount + for k, ent in pairs(entities) do + ent.amount = ent.amount + floor(amount/#entities) + end + break + end + end + end + else + saved = amount + end + end + debug("Total amount: ".._total) + debug("Leaving spawn_resource_liquid") + return _total +end + +local spawnerTable = nil + +local function initSpawnerTable() + if spawnerTable == nil then + spawnerTable = {} + spawnerTable["biter-spawner"] = game.entity_prototypes["biter-spawner"] ~= nil + spawnerTable["bob-biter-spawner"] = game.entity_prototypes["bob-biter-spawner"] ~= nil + spawnerTable["spitter-spawner"] = game.entity_prototypes["spitter-spawner"] ~= nil + spawnerTable["bob-spitter-spawner"] = game.entity_prototypes["bob-spitter-spawner"] ~= nil + end +end + +local function spawn_entity(surface, ent, r_config, x, y) + if not use_RSO_biter_spawning then return end + local size=rgen:random(r_config.size.min, r_config.size.max) + + local _total = 0 + local r_distance = distance({x=0,y=0},{x=x/REGION_TILE_SIZE,y=y/REGION_TILE_SIZE}) + + local distanceMultiplier = math.min(r_distance^r_config.size_per_region_factor, 5) + if r_config.size_per_region_factor then + size = size*distanceMultiplier + end + + size = size * enemy_base_size_multiplier + + debug("Entering spawn_entity "..ent.." "..x..","..y.." "..size) + + local maxAttemptCount = 5 + local distancePerAttempt = 0.2 + + initSpawnerTable() + + for i=1,size do + for attempt = 1, maxAttemptCount do + local richness=r_config.richness*(r_distance^richness_distance_factor) + local max_d = floor(CHUNK_SIZE*(0.5 + distancePerAttempt*attempt)) + local s_x = x + rgen:random(0, floor(max_d - r_config.clear_range[1])) - max_d/2 + r_config.clear_range[1] + local s_y = y + rgen:random(0, floor(max_d - r_config.clear_range[2])) - max_d/2 + r_config.clear_range[2] + + if surface.get_tile(s_x, s_y).valid then + + remove_trees(surface, s_x, s_y, r_config.clear_range[1], r_config.clear_range[2]) + + local spawnerName = nil + + if spawner_probability_edge > 0 then + + bigSpawnerChance = rgen:random() + + if rgen:random() < spawner_probability_edge then + if ( useBobEntity and bigSpawnerChance > 0.75 ) then + spawnerName = "bob-biter-spawner" + else + spawnerName = "biter-spawner" + end + else + if ( useBobEntity and bigSpawnerChance > 0.75 ) then + spawnerName = "bob-spitter-spawner" + else + spawnerName = "spitter-spawner" + end + end + end + + if spawnerName and spawnerTable[spawnerName] then + if surface.can_place_entity{name=spawnerName, position={s_x, s_y}} then + _total = _total + richness + debug(spawnerName.." @ "..s_x..","..s_y.." placed on "..attempt.." attempt") + + surface.create_entity{name=spawnerName, position={s_x, s_y}, force=game.forces[r_config.force], amount=floor(richness)}--, direction=rgen:random(4) + -- else + -- debug("Entity "..spawnerName.." spawn failed") + break; + else + if attempt == maxAttemptCount then + debug(spawnerName.." @ "..s_x..","..s_y.." failed to spawn") + end + end + else + debug("Entity "..spawnerName.." doesn't exist") + end + end + end + + if r_config.sub_spawn_probability then + local sub_spawn_prob = r_config.sub_spawn_probability*math.min(r_config.sub_spawn_max_distance_factor, r_config.sub_spawn_distance_factor^r_distance) + if rgen:random() < sub_spawn_prob then + for i=1,(rgen:random(r_config.sub_spawn_size.min, r_config.sub_spawn_size.max)*distanceMultiplier) do + local allotment_max = 0 + -- build table + for k,v in pairs(r_config.sub_spawns) do + if not v.min_distance or r_distance > v.min_distance then + local allotment = v.allotment + if v.allotment_distance_factor then + allotment = allotment * (v.allotment_distance_factor^r_distance) + end + v.allotment_range ={min = allotment_max, max = allotment_max + allotment} + allotment_max = allotment_max + allotment + else + v.allotment_range = nil + end + end + local sub_type = rgen:random(0, allotment_max) + for sub_spawn,v in pairs(r_config.sub_spawns) do + if v.allotment_range and sub_type >= v.allotment_range.min and sub_type <= v.allotment_range.max then + for attempt = 1, maxAttemptCount do + local max_d = floor(CHUNK_SIZE*distancePerAttempt*attempt) + s_x = x + rgen:random(max_d) - max_d/2 + s_y = y + rgen:random(max_d) - max_d/2 + remove_trees(surface, s_x, s_y, v.clear_range[1], v.clear_range[2]) + if surface.can_place_entity{name=sub_spawn, position={s_x, s_y}} then + surface.create_entity{name=sub_spawn, position={s_x, s_y}, force=game.forces[r_config.force]}--, direction=rgen:random(4) + debug("Rolled subspawn "..sub_spawn.." @ "..s_x..","..s_x.." after "..attempt.." attempts") + break; + else + if attempt == maxAttemptCount then + debug("Rolling subspawn "..sub_spawn.." @ "..s_x..","..s_x.." failed") + end + end + end + break + end + end + end + end + end + end + debug("Total amount: ".._total) + debug("Leaving spawn_entity") +end + +--[[ EVENT/INIT METHODS ]]-- + +local function spawn_starting_resources( surface, index ) + + if global.startingAreas[index].spawned then return end + if surface.map_gen_settings.starting_area == "none" and not ignoreMapGenSettings then return end -- starting area disabled by map gen + if starting_area_size < 0.1 then return end -- skip spawning if starting area is to small + + local position = global.startingAreas[index] + + rgen = rng_for_reg_pos( position ) + local status = true + for index,v in ipairs(configIndexed) do + if v.starting then + local prob = rgen:random() -- probability that this resource is spawned + debug("starting resource probability rolled "..prob) + if v.starting.probability > 0 and prob <= v.starting.probability then + local total = 0 + local radius = 25 + local min_threshold = 0 + + if v.type == "resource-ore" then + min_threshold = v.starting.richness * rgen:random(5, 10) -- lets make sure that there is at least 10-15 times starting richness ore at start + elseif v.type == "resource-liquid" then + min_threshold = v.starting.richness * 0.5 * v.starting.size + end + + while (radius < 200) and (total < min_threshold) do + local angle = rgen:random() * pi * 2 + local dist = rgen:random() * 30 + radius * 2 + local pos = { x = floor(cos(angle) * dist) + position.x, y = floor(sin(angle) * dist) + position.y } + if v.type == "resource-ore" then + total = total + spawn_resource_ore(surface, v.name, pos, v.starting.size, v.starting.richness, true) + elseif v.type == "resource-liquid" then + total = total + spawn_resource_liquid(surface, v.name, pos, v.starting.size, v.starting.richness, true) + end + radius = radius + 10 + end + if total < min_threshold then + status = false + end + end + end + end + + global.startingAreas[index].spawned = true +end + +local function modifyMinMax(value, mod) + value.min = round( value.min * mod ) + value.max = round( value.max * mod ) +end + +local function prebuild_config_data(surface) + if index_is_built then return false end + + local mapGenSettings = nil + + if not ignoreMapGenSettings then + mapGenSettings = surface.map_gen_settings + end + local autoPlaceSettings = nil + if mapGenSettings then + autoPlaceSettings = mapGenSettings.autoplace_controls + end + + configIndexed = {} + -- build additional indexed array to the associative array + for res_name, res_conf in pairs(config) do + if res_conf.valid then -- only add valid resources + res_conf.name = res_name + + local settingsForResource = nil + local isEntity = (res_conf.type == "entity") + local addResource = true + + local autoplaceName = res_name + + if res_conf.autoplace_name then + autoplaceName = res_conf.autoplace_name + end + + if autoPlaceSettings then + settingsForResource = autoPlaceSettings[autoplaceName] + end + + if settingsForResource then + local allotmentMod = nil + local sizeMod = nil + if isEntity then + allotmentMod = entityFrequencyMultiplier[settingsForResource.frequency] + sizeMod = entitySizeMultiplier[settingsForResource.size] + else + allotmentMod =frequencyAllotmentMultiplier[settingsForResource.frequency] + sizeMod = sizeMultiplier[settingsForResource.size] + end + + local richnessMod = richnessMultiplier[settingsForResource.richness] + + + debug(res_name .. " allotment mod " .. allotmentMod .. " size mod " .. sizeMod .. " richness mod " .. richnessMod ) + + + if allotmentMod then + if isEntity then + res_conf.absolute_probability = res_conf.absolute_probability * allotmentMod + debug("Entity chance modified to "..res_conf.absolute_probability) + else + res_conf.allotment = round( res_conf.allotment * allotmentMod ) + end + end + + if sizeMod ~= nil and sizeMod == 0 then + addResource = false + end + + if sizeMod then + modifyMinMax(res_conf.size, sizeMod) + + if res_conf.starting then + res_conf.starting.size = round( res_conf.starting.size * sizeMod ) + end + + if isEntity then + if res_conf.sub_spawn_size then + modifyMinMax(res_conf.sub_spawn_size, sizeMod) + end + modifyMinMax(res_conf.spawns_per_region, sizeMod) + end + end + + if richnessMod then + if type == "resource-ore" then + res_conf.richness = round( res_conf.richness * richnessMod ) + elseif type == "resource-liquid" then + modifyMinMax(res_conf.richness, richnessMod) + end + + if res_conf.starting then + res_conf.starting.richness = round( res_conf.starting.richness * richnessMod ) + end + end + end + + if addResource then + configIndexed[#configIndexed + 1] = res_conf + if res_conf.multi_resource and multi_resource_active then + local new_list = {} + for sub_res_name, allotment in pairs(res_conf.multi_resource) do + if config[sub_res_name] and config[sub_res_name].valid then + new_list[#new_list+1] = {name = sub_res_name, allotment = allotment} + end + end + table.sort(new_list, function(a, b) return a.name < b.name end) + res_conf.multi_resource = new_list + else + res_conf.multi_resource_chance = nil + end + end + end + end + + table.sort(configIndexed, function(a, b) return a.name < b.name end) + + local pr=0 + for index,v in pairs(config) do + if v.along_resource_probability then + v.along_resource_probability_range={min=pr, max=pr+v.along_resource_probability} + pr=pr+v.along_resource_probability + end + if v.allotment and v.allotment > 0 then + v.allotment_range={min=max_allotment, max=max_allotment+v.allotment} + max_allotment=max_allotment+v.allotment + end + end + + if mapGenSettings and mapGenSettings.starting_area then + local multiplier = startingAreaMultiplier[mapGenSettings.starting_area] + if multiplier ~= nil then + starting_area_size = starting_area_size * multiplier + debug("Starting area "..starting_area_size) + end + end + + index_is_built = true +end + +-- set up the probabilty segments from which to roll between for biter and spitter spawners +local function calculate_spawner_ratio() + if (biter_ratio_segment ~= 0 and spitter_ratio_segment ~= 0) and biter_ratio_segment >= 0 and spitter_ratio_segment >= 0 then + spawner_probability_edge=biter_ratio_segment/(biter_ratio_segment+spitter_ratio_segment) -- normalize to between 0 and 1 + end +end + +local function checkConfigForInvalidResources() + --make sure that every resource in the config is actually available. + --call this function, before the auxiliary config is prebuilt! + if index_is_built then return end + + local prototypes = game.entity_prototypes + + for resourceName, resourceConfig in pairs(config) do + if prototypes[resourceName] or resourceConfig.type == "entity" then + resourceConfig.valid = true + else + -- resource was in config, but it doesn't exist in game files anymore - mark it invalid + resourceConfig.valid = false + + table.insert(invalidResources, "Resource not available: " .. resourceName) + debug("Resource not available: " .. resourceName) + end + + if resourceConfig.valid and resourceConfig.type ~= "entity" then + if prototypes[resourceName].autoplace_specification == nil then + resourceConfig.valid = false + debug("Resource "..resourceName.." invalidated - autoplace not present") + end + end + end +end + +local function roll_region(c_x, c_y) + --in what region is this chunk? + local r_x=floor(c_x/REGION_TILE_SIZE) + local r_y=floor(c_y/REGION_TILE_SIZE) + local r_data = nil + --don't spawn stuff in starting area + if isInStartingArea( c_x/REGION_TILE_SIZE, c_y/REGION_TILE_SIZE ) then + return false + end + + if global.regions[r_x] and global.regions[r_x][r_y] then + r_data = global.regions[r_x][r_y] + else + --if this chunk is the first in its region to be generated + if not global.regions[r_x] then global.regions[r_x] = {} end + global.regions[r_x][r_y]={} + r_data = global.regions[r_x][r_y] + rgen = rng_for_reg_pos{x=r_x,y=r_y} + + local rollCount = math.ceil(#configIndexed / 10) - 1 -- 0 based counter is more convenient here + rollCount = math.min(rollCount, 3) + + for rollNumber = 0,rollCount do + + local resourceChance = absolute_resource_chance - rollNumber * 0.1 + --absolute chance to spawn resource + local abct = rgen:random() + debug("Rolling resource "..abct.." against "..resourceChance.." roll "..rollNumber) + if abct <= resourceChance then + local res_type=rgen:random(1, max_allotment) + for index,v in ipairs(configIndexed) do + if v.allotment_range and ((res_type >= v.allotment_range.min) and (res_type <= v.allotment_range.max)) then + debug("Rolled primary resource "..v.name.." with res_type="..res_type.." @ "..r_x..","..r_y) + local num_spawns=rgen:random(v.spawns_per_region.min, v.spawns_per_region.max) + local last_spawn_coords = {} + local along_ + for i=1,num_spawns do + local c_x, c_y = find_random_chunk(r_x, r_y) + if not r_data[c_x] then r_data[c_x] = {} end + if not r_data[c_x][c_y] then r_data[c_x][c_y] = {} end + local c_data = r_data[c_x][c_y] + c_data[#c_data+1]={v.name, rollNumber} + last_spawn_coords[#last_spawn_coords+1] = {c_x, c_y} + debug("Rolled primary chunk "..v.name.." @ "..c_x.."."..c_y.." reg: "..r_x..","..r_y) + -- Along resource spawn, only once + if i == 1 then + local am_roll = rgen:random() + for index,vv in ipairs(configIndexed) do + if vv.along_resource_probability_range and am_roll >= vv.along_resource_probability_range.min and am_roll <= vv.along_resource_probability_range.max then + c_data = r_data[c_x][c_y] + c_data[#c_data+1]={vv.name, rollNumber} + debug("Rolled along "..vv.name.." @ "..c_x.."."..c_y.." reg: "..r_x..","..r_y) + end + end + end + end + -- roll multiple resources in same region + local deep=0 + while v.multi_resource_chance and rgen:random() <= v.multi_resource_chance*(multi_resource_chance_diminish^deep) do + deep = deep + 1 + local max_allotment = 0 + for index,sub_res in pairs(v.multi_resource) do max_allotment=max_allotment+sub_res.allotment end + + local res_type=rgen:random(1, max_allotment) + local min=0 + for _, sub_res in pairs(v.multi_resource) do + if (res_type >= min) and (res_type <= sub_res.allotment + min) then + local last_coords = last_spawn_coords[rgen:random(1, #last_spawn_coords)] + local c_x, c_y = find_random_neighbour_chunk(last_coords[1], last_coords[2]) -- in same as primary resource chunk + if not r_data[c_x] then r_data[c_x] = {} end + if not r_data[c_x][c_y] then r_data[c_x][c_y] = {} end + local c_data = r_data[c_x][c_y] + c_data[#c_data+1]={sub_res.name, deep} + debug("Rolled multiple "..sub_res.name..":"..deep.." with res_type="..res_type.." @ "..c_x.."."..c_y.." reg: "..r_x.."."..r_y) + break + else + min = min + sub_res.allotment + end + end + end + break + end + end + + end + end + -- roll for absolute_probability - this rolls the enemies + + for index,v in ipairs(configIndexed) do + if v.absolute_probability then + local prob_factor = 1 + if v.probability_distance_factor then + prob_factor = math.min(v.max_probability_distance_factor, v.probability_distance_factor^distance({x=0,y=0},{x=r_x,y=r_y})) + end + local abs_roll = rgen:random() + if abs_roll 10 then + global.startingAreas[1].spawned = true + end + end + + calculate_spawner_ratio() + spawn_starting_resources(surface, 1 ) + + initDone = true + + if game.surfaces["nauvis"].map_gen_settings.autoplace_controls["iron-ore"].size ~= "none" then + game.players[1].print("RSO WARNING - VANILLA iron-ore GEN IS NOT DISABLED!") + end + if game.surfaces["nauvis"].map_gen_settings.autoplace_controls["copper-ore"].size ~= "none" then + game.players[1].print("RSO WARNING - VANILLA copper-ore GEN IS NOT DISABLED!") + end + if game.surfaces["nauvis"].map_gen_settings.autoplace_controls["crude-oil"].size ~= "none" then + game.players[1].print("RSO WARNING - VANILLA crude-oil GEN IS NOT DISABLED!") + end + if game.surfaces["nauvis"].map_gen_settings.autoplace_controls["enemy-base"].size ~= "none" then + game.players[1].print("RSO WARNING - VANILLA enemy-base GEN IS NOT DISABLED!") + end + if game.surfaces["nauvis"].map_gen_settings.autoplace_controls["stone"].size ~= "none" then + game.players[1].print("RSO WARNING - VANILLA stone GEN IS NOT DISABLED!") + end + if game.surfaces["nauvis"].map_gen_settings.autoplace_controls["coal"].size ~= "none" then + game.players[1].print("RSO WARNING - VANILLA coal GEN IS NOT DISABLED!") + end + end + + script.on_event(defines.events.on_tick, nil) +end + +local function delayedInit() + script.on_event(defines.events.on_tick, init) +end + +function RSO_ChunkGenerated(event) + local c_x = event.area.left_top.x + local c_y = event.area.left_top.y + + init() + + roll_region(c_x, c_y) + roll_chunk(event.surface, c_x, c_y) +end + +function RSO_PlayerCreated(event) + init() + local player = game.players[event.player_index] + + if debug_enabled then + printResourceProbability(player) + end +end + +if ENABLE_RSO then + script.on_load(delayedInit) + Event.register(defines.events.on_chunk_generated, RSO_ChunkGenerated) + Event.register(defines.events.on_player_created, RSO_PlayerCreated) +end \ No newline at end of file diff --git a/rso_resource_config.lua b/rso_resource_config.lua new file mode 100644 index 0000000..8168506 --- /dev/null +++ b/rso_resource_config.lua @@ -0,0 +1,159 @@ + +local function fillVanillaConfig() + + config["iron-ore"] = { + type="resource-ore", + + -- general spawn params + allotment=100, -- how common resource is + spawns_per_region={min=1, max=1}, --number of chunks + richness=18000, -- resource_ore has only one richness value - resource-liquid has min/max + + size={min=20, max=30}, -- rough radius of area, too high value can produce square shaped areas + min_amount=350, + + -- resource provided at starting location + -- probability: 1 = 100% chance to be in starting area + -- 0 = resource is not in starting area + starting={richness=8000, size=25, probability=1}, + + multi_resource_chance=0.20, -- absolute value + multi_resource={ + ["iron-ore"] = 2, -- ["resource_name"] = allotment + ['copper-ore'] = 4, + ["coal"] = 4, + ["stone"] = 4, + } + } + + config["copper-ore"] = { + type="resource-ore", + + allotment=100, + spawns_per_region={min=1, max=1}, + richness=16000, + size={min=20, max=30}, + min_amount=350, + + starting={richness=6000, size=25, probability=1}, + + multi_resource_chance=0.20, + multi_resource={ + ["iron-ore"] = 4, + ['copper-ore'] = 2, + ["coal"] = 4, + ["stone"] = 4, + } + } + + config["coal"] = { + type="resource-ore", + + allotment=80, + + spawns_per_region={min=1, max=1}, + size={min=15, max=25}, + richness=13000, + min_amount=350, + + starting={richness=6000, size=20, probability=1}, + + multi_resource_chance=0.30, + multi_resource={ + ["crude-oil"] = 1, + ["iron-ore"] = 3, + ['copper-ore'] = 3, + } + } + + config["stone"] = { + type="resource-ore", + + allotment=60, + spawns_per_region={min=1, max=1}, + richness=11000, + size={min=15, max=20}, + min_amount=250, + + starting={richness=5000, size=16, probability=1}, + + multi_resource_chance=0.30, + multi_resource={ + ["coal"] = 4, + ["iron-ore"] = 3, + ['copper-ore'] = 3, + } + } + + config["crude-oil"] = { + type="resource-liquid", + minimum_amount=10000, + allotment=70, + spawns_per_region={min=1, max=2}, + richness={min=10000, max=30000}, -- richness per resource spawn + size={min=2, max=5}, + + starting={richness=20000, size=2, probability=1}, + + multi_resource_chance=0.20, + multi_resource={ + ["coal"] = 4, + } + } +end + +local function fillEnemies() + + config["enemy-base"] = { + type="entity", + force="enemy", + clear_range = {6, 6}, + + spawns_per_region={min=2,max=4}, + size={min=2,max=4}, + size_per_region_factor=0.4, + richness=1, + + absolute_probability=absolute_enemy_chance, -- chance to spawn in region + probability_distance_factor=1.15, -- relative increase per region + max_probability_distance_factor=3.0, -- absolute value + + along_resource_probability=0.20, -- chance to spawn in resource chunk anyway, absolute value. Can happen once per resource. + + sub_spawn_probability=0.3, -- chance for this entity to spawn anything from sub_spawns table, absolute value + sub_spawn_size={min=1, max=2}, -- in same chunk + sub_spawn_distance_factor=1.04, + sub_spawn_max_distance_factor=3, + sub_spawns={ + ["small-worm-turret"]={ + min_distance=2, + allotment=200, + allotment_distance_factor=0.9, + clear_range = {2, 2}, + }, + ["medium-worm-turret"]={ + min_distance=4, + allotment=100, + allotment_distance_factor=1.05, + clear_range = {2, 2}, + }, + ["big-worm-turret"]={ + min_distance=6, + allotment=100, + allotment_distance_factor=1.15, + clear_range = {2, 2}, + } + } + } + +end + +function loadResourceConfig() + + config={} + + fillVanillaConfig() + fillEnemies() + + return config +end diff --git a/separate_spawns.lua b/separate_spawns.lua new file mode 100644 index 0000000..acec4fb --- /dev/null +++ b/separate_spawns.lua @@ -0,0 +1,644 @@ +-- Separate spawns +-- Code that handles everything regarding giving each player a separate spawn +-- Includes the GUI stuff + +require("event") + +-- When a new player is created, present the spawn options +-- Assign them to the main force so they can communicate with the team +-- without shouting. +function PlayerCreated(event) + local player = game.players[event.player_index] + player.force = MAIN_FORCE + DisplaySpawnOptions(player) +end + + +-- Check if the player has a different spawn point than the default one +-- Make sure to give the default starting items +function PlayerRespawned(event) + local player = game.players[event.player_index] + + -- If a player has an active spawn, use it. + if (DoesPlayerHaveActiveCustomSpawn(player)) then + player.teleport(global.playerSpawns[player.name]) + end + + -- Display the respawn continue option + DisplayRespawnContinueOption(player) +end + +-- Create the appropriate force & spawn when player selects their choice +function SpawnGuiClick(event) + local player = game.players[event.player_index] + local buttonClicked = event.element.name + + -- Only clear gui if a valid button was clicked!! + if ((buttonClicked == "normal_spawn") or + (buttonClicked == "isolated_spawn") or + (buttonClicked == "isolated_spawn_far") or + (buttonClicked == "new_force") or + (buttonClicked == "new_force_far") or + (buttonClicked == "respawn_continue") or + (buttonClicked == "respawn_change") or + (buttonClicked == "respawn_custom_team") or + (buttonClicked == "respawn_custom_spawn") or + (buttonClicked == "respawn_surpise") or + (buttonClicked == "respawn_mainforce")) then + + if not global.spawnDebugEnabled then + if (player.gui.center.spawn_opts ~= nil) then + player.gui.center.spawn_opts.destroy() + end + if (player.gui.center.respawn_opts ~= nil) then + player.gui.center.respawn_opts.destroy() + end + if (player.gui.center.respawn_continue_opts ~= nil) then + player.gui.center.respawn_continue_opts.destroy() + end + end + end + + -- In this option, the vanilla spawn is used and the player is + -- part of the main force. + if (buttonClicked == "normal_spawn") then + player.force = MAIN_FORCE + GivePlayerStarterItems(player) + SendBroadcastMsg(player.name .. " joined the main force!") + + -- In this option, the player gets a separate spawn point + -- but is still part of the main force. + elseif ((buttonClicked == "isolated_spawn") or (buttonClicked == "isolated_spawn_far")) then + player.force = MAIN_FORCE + + -- Create a new spawn point + local newSpawn = {} + if (buttonClicked == "isolated_spawn_far") then + newSpawn = FindUngeneratedCoordinates(FAR_MIN_DIST,FAR_MAX_DIST) + else + newSpawn = FindMapEdge(GetRandomVector()) + end + global.playerSpawns[player.name] = newSpawn + global.activePlayerSpawns[player.name] = true + + SendPlayerToNewSpawnAndCreateIt(player, newSpawn) + if (buttonClicked == "isolated_spawn") then + SendBroadcastMsg(player.name .. " joined the main force from a distance!") + elseif (buttonClicked == "isolated_spawn_far") then + SendBroadcastMsg(player.name .. " joined the main force from a great distance!") + end + + -- In this option, the player is given a new force and a + -- separate spawn point + elseif ((buttonClicked == "new_force") or (buttonClicked == "new_force_far")) then + + -- Create a new force using the player's name + local newForce = CreatePlayerCustomForce(player) + + -- Create a new spawn point + local newSpawn = {} + if (buttonClicked == "new_force_far") then + newSpawn = FindUngeneratedCoordinates(FAR_MIN_DIST,FAR_MAX_DIST) + else + newSpawn = FindMapEdge(GetRandomVector()) + end + global.playerSpawns[player.name] = newSpawn + global.activePlayerSpawns[player.name] = true + + -- Set the new spawn point + if (newForce ~= nil) then + newForce.set_spawn_position(newSpawn, "nauvis") + end + + SendPlayerToNewSpawnAndCreateIt(player, newSpawn) + SendBroadcastMsg(player.name .. " is going it alone!") + + -- Continue to respawn on your own team at that location + elseif (buttonClicked == "respawn_continue") then + GivePlayerItems(player) + + + -- If changing your spawn behavior + elseif (buttonClicked == "respawn_change") then + if (DoesPlayerHaveCustomSpawn(player)) then + DisplayRespawnOptions(player) + else + DisplaySpawnOptions(player) + end + + -- Respawn with the main force in the default location + elseif (buttonClicked == "respawn_mainforce") then + + -- Remove custom force if it exists + if (player.force.name ~= MAIN_FORCE) then + game.merge_forces(player.name, MAIN_FORCE) + end + + -- Deactivate the stored spawn point + ActivatePlayerCustomSpawn(player, false) + player.teleport(player.force.get_spawn_position("nauvis")) + GivePlayerStarterItems(player) + SendBroadcastMsg(player.name .. " is returning to base!") + + -- Respawn in your already generated custom spawn area on the main team + elseif (buttonClicked == "respawn_custom_spawn") then + + -- Remove custom force if it exists + if (player.force.name ~= MAIN_FORCE) then + game.merge_forces(player.name, MAIN_FORCE) + end + + -- Activate the stored spawn point + ActivatePlayerCustomSpawn(player, true) + SendPlayerToActiveSpawn(player) + GivePlayerStarterItems(player) + SendBroadcastMsg(player.name .. " is returning to their outpost!") + + -- Respawn in your already generated custom spawn area but on your own + -- force. This force is created new if it doesn't exist. + elseif (buttonClicked == "respawn_custom_team") then + + -- Create a new force using the player's name + local newForce = CreatePlayerCustomForce(player) + + -- Set the new spawn point + if (newForce ~= nil) then + newForce.set_spawn_position(global.playerSpawns[player.name], "nauvis") + end + + -- Activate the stored spawn point + ActivatePlayerCustomSpawn(player, true) + SendPlayerToActiveSpawn(player) + GivePlayerStarterItems(player) + SendBroadcastMsg(player.name .. " is returning to their outpost alone!") + + -- lol wut + elseif (buttonClicked == "respawn_surpise") then + + -- Remove custom force if it exists + if (player.force.name ~= MAIN_FORCE) then + game.merge_forces(player.name, MAIN_FORCE) + end + + -- Activate the stored spawn point + SendPlayerToRandomSpawn(player) + GivePlayerStarterItems(player) + SendBroadcastMsg(player.name .. " is surprised!") + + end +end + +-- This is the main function that creates the spawn area +-- Provides resources, land and a safe zone +function GenerateChunk(event) + local surface = event.surface + if surface.name ~= "nauvis" then return end + local chunkArea = event.area + + -- This handles chunk generation near player spawns + -- If it is near a player spawn, it does a few things like make the area + -- safe and provide a guaranteed area of land and water tiles. + for name,spawnPos in pairs(global.playerSpawns) do + + local landArea = {left_top= + {x=spawnPos.x-ENFORCE_LAND_AREA_TILE_DIST, + y=spawnPos.y-ENFORCE_LAND_AREA_TILE_DIST}, + right_bottom= + {x=spawnPos.x+ENFORCE_LAND_AREA_TILE_DIST, + y=spawnPos.y+ENFORCE_LAND_AREA_TILE_DIST}} + + local safeArea = {left_top= + {x=spawnPos.x-SAFE_AREA_TILE_DIST, + y=spawnPos.y-SAFE_AREA_TILE_DIST}, + right_bottom= + {x=spawnPos.x+SAFE_AREA_TILE_DIST, + y=spawnPos.y+SAFE_AREA_TILE_DIST}} + + local warningArea = {left_top= + {x=spawnPos.x-WARNING_AREA_TILE_DIST, + y=spawnPos.y-WARNING_AREA_TILE_DIST}, + right_bottom= + {x=spawnPos.x+WARNING_AREA_TILE_DIST, + y=spawnPos.y+WARNING_AREA_TILE_DIST}} + + local chunkAreaCenter = {x=chunkArea.left_top.x+(CHUNK_SIZE/2), + y=chunkArea.left_top.y+(CHUNK_SIZE/2)} + + + + -- Make chunks near a spawn safe by removing enemies + if CheckIfInArea(chunkAreaCenter,safeArea) then + for _, entity in pairs(surface.find_entities_filtered{area = chunkArea, force = "enemy"}) do + entity.destroy() + end + + -- Create a warning area with reduced enemies + elseif CheckIfInArea(chunkAreaCenter,warningArea) then + local counter = 0 + for _, entity in pairs(surface.find_entities_filtered{area = chunkArea, force = "enemy"}) do + if ((counter % WARN_AREA_REDUCTION_RATIO) ~= 0) then + entity.destroy() + end + counter = counter + 1 + end + + -- Remove all big and huge worms + for _, entity in pairs(surface.find_entities_filtered{area = chunkArea, name = "medium-worm-turret"}) do + entity.destroy() + end + for _, entity in pairs(surface.find_entities_filtered{area = chunkArea, name = "big-worm-turret"}) do + entity.destroy() + end + + end + + -- Fill in any water to make sure we have guaranteed land mass at the spawn point. + if CheckIfInArea(chunkAreaCenter,landArea) then + + -- remove trees in the immediate areas? + for key, entity in pairs(surface.find_entities_filtered({area=chunkArea, type= "tree"})) do + if ((spawnPos.x - entity.position.x)^2 + (spawnPos.y - entity.position.y)^2 < ENFORCE_LAND_AREA_TILE_DIST^2) then + entity.destroy() + end + end + + CreateCropCircle(surface, spawnPos, chunkArea, ENFORCE_LAND_AREA_TILE_DIST) + end + + -- Provide a guaranteed spot of water to use for power generation + if CheckIfInArea(spawnPos,chunkArea) then + local waterTiles = {{name = "water", position ={spawnPos.x+0,spawnPos.y-30}}, + {name = "water", position ={spawnPos.x+1,spawnPos.y-30}}, + {name = "water", position ={spawnPos.x+2,spawnPos.y-30}}, + {name = "water", position ={spawnPos.x+3,spawnPos.y-30}}, + {name = "water", position ={spawnPos.x+4,spawnPos.y-30}}, + {name = "water", position ={spawnPos.x+5,spawnPos.y-30}}, + {name = "water", position ={spawnPos.x+6,spawnPos.y-30}}, + {name = "water", position ={spawnPos.x+7,spawnPos.y-30}}, + {name = "water", position ={spawnPos.x+8,spawnPos.y-30}}} + -- DebugPrint("Setting water tiles in this chunk! " .. chunkArea.left_top.x .. "," .. chunkArea.left_top.y) + surface.set_tiles(waterTiles) + end + end +end + +function InitSpawnGlobalsAndForces() + -- Containes an array of all player spawns + -- A secondary array tracks whether the character will respawn there. + if (global.playerSpawns == nil) then + global.playerSpawns = {} + global.activePlayerSpawns = {} + end + + game.create_force(MAIN_FORCE) + game.forces[MAIN_FORCE].set_spawn_position(game.forces["player"].get_spawn_position("nauvis"), "nauvis") + SetCeaseFireBetweenAllForces() +end + +function DisplaySpawnOptions(player) + player.gui.center.add{name = "spawn_opts", + type = "frame", + direction = "vertical", + caption="Spawn Options"} + local spawnGui = player.gui.center.spawn_opts + + spawnGui.style.maximal_width = 450 + spawnGui.style.maximal_height = 650 + + spawnGui.add{name = "warning_lbl1", type = "label", + caption="You can only generate one custom spawn point!"} + spawnGui.add{name = "warning_spacer", type = "label", + caption=" "} + ApplyStyle(spawnGui.warning_lbl1, my_warning_style) + ApplyStyle(spawnGui.warning_spacer, my_label_style) + + spawnGui.add{name = "normal_spawn", + type = "button", + caption="Default Spawn"} + spawnGui.add{name = "normal_spawn_lbl1", type = "label", + caption="This is the default spawn behavior of a vanilla game."} + spawnGui.add{name = "normal_spawn_lbl2", type = "label", + caption="You join the default team in the center of the map."} + spawnGui.add{name = "normal_spawn_spacer", type = "label", + caption=" "} + ApplyStyle(spawnGui.normal_spawn_lbl1, my_label_style) + ApplyStyle(spawnGui.normal_spawn_lbl2, my_label_style) + ApplyStyle(spawnGui.normal_spawn_spacer, my_label_style) + + spawnGui.add{name = "isolated_spawn", + type = "button", + caption="Isolated Spawn"} + spawnGui.add{name = "isolated_spawn_far", + type = "button", + caption="Isolated Spawn (Far Away)"} + spawnGui.add{name = "isolated_spawn_lbl1", type = "label", + caption="You are spawned in a new area, with starting resources."} + spawnGui.add{name = "isolated_spawn_lbl2", type = "label", + caption="You will still be part of the default team."} + spawnGui.add{name = "isolated_spawn_spacer", type = "label", + caption=" "} + ApplyStyle(spawnGui.isolated_spawn_lbl1, my_label_style) + ApplyStyle(spawnGui.isolated_spawn_lbl2, my_label_style) + ApplyStyle(spawnGui.isolated_spawn_spacer, my_label_style) + + + if (ENABLE_OTHER_TEAMS) then + spawnGui.add{name = "new_force", + type = "button", + caption="Separate Team"} + spawnGui.add{name = "new_force_far", + type = "button", + caption="Separate Team (Far Away)"} + spawnGui.add{name = "new_force_lbl1", type = "label", + caption="You are spawned in a new area, with starting resources."} + spawnGui.add{name = "new_force_lbl2", type = "label", + caption="You will be on your own team. (No shared vision or research with others.)"} + spawnGui.add{name = "new_force_lbl3", type = "label", + caption="Do not choose this option if you are new to the game!"} + spawnGui.add{name = "new_force_spacer", type = "label", + caption=" "} + ApplyStyle(spawnGui.new_force_lbl1, my_label_style) + ApplyStyle(spawnGui.new_force_lbl2, my_warning_style) + ApplyStyle(spawnGui.new_force_lbl3, my_warning_style) + ApplyStyle(spawnGui.new_force_spacer, my_label_style) + + + + spawnGui.add{name = "note_lbl1", type = "label", + caption="All members of a team share map vision and research."} + spawnGui.add{name = "note_lbl2", type = "label", + caption="To talk to someone on a different team, you need to use /s to shout."} + spawnGui.add{name = "note_lbl3", type = "label", + caption="All teams are neutral. This is still a cooperative PvE game... NOT PVP!"} + ApplyStyle(spawnGui.note_lbl1, my_note_style) + ApplyStyle(spawnGui.note_lbl2, my_note_style) + ApplyStyle(spawnGui.note_lbl3, my_note_style) + end + + spawnGui.add{name = "note_lbl4", type = "label", + caption="Far away spawn is between " .. FAR_MIN_DIST*CHUNK_SIZE .. "-" .. FAR_MAX_DIST*CHUNK_SIZE .. " tiles away from the center of the map."} + spawnGui.add{name = "note_lbl5", type = "label", + caption="Isolated spawns are dangerous! You will have to fight to reach other players."} + spawnGui.add{name = "note_lbl6", type = "label", + caption="You can change your spawn options when you die."} + + ApplyStyle(spawnGui.note_lbl4, my_note_style) + ApplyStyle(spawnGui.note_lbl5, my_note_style) +end + +function DisplayRespawnContinueOption(player) + + player.gui.center.add{name = "respawn_continue_opts", + type = "frame", + direction = "vertical", + caption="Respawn Options"} + local respawnGui = player.gui.center.respawn_continue_opts + + respawnGui.style.maximal_width = 450 + respawnGui.style.maximal_height = 550 + + respawnGui.add{name = "respawn_continue", + type = "button", + caption="Continue"} + respawnGui.add{name = "respawn_continue_lbl1", type = "label", + caption="Continue at your current spawn location."} + respawnGui.add{name = "respawn_continue_spacer", type = "label", + caption=" "} + ApplyStyle(respawnGui.respawn_continue_lbl1, my_label_style) + ApplyStyle(respawnGui.respawn_continue_spacer, my_label_style) + + respawnGui.add{name = "respawn_change", + type = "button", + caption="Change Spawn"} + respawnGui.add{name = "respawn_change_lbl1", type = "label", + caption="Allow you to change your spawn and team."} + respawnGui.add{name = "respawn_change_spacer", type = "label", + caption=" "} + ApplyStyle(respawnGui.respawn_change_lbl1, my_label_style) + ApplyStyle(respawnGui.respawn_change_spacer, my_label_style) +end + +function DisplayRespawnOptions(player) + + player.gui.center.add{name = "respawn_opts", + type = "frame", + direction = "vertical", + caption="Respawn Options"} + local respawnGui = player.gui.center.respawn_opts + + respawnGui.style.maximal_width = 450 + respawnGui.style.maximal_height = 750 + + -- Basically a cancel button to avoid choosing a different spawn. + respawnGui.add{name = "respawn_continue", + type = "button", + caption="Cancel"} + respawnGui.add{name = "respawn_continue_lbl1", type = "label", + caption="Continue with current spawn."} + respawnGui.add{name = "respawn_continue_spacer", type = "label", + caption=" "} + ApplyStyle(respawnGui.respawn_continue_lbl1, my_label_style) + ApplyStyle(respawnGui.respawn_continue_spacer, my_label_style) + + respawnGui.add{name = "respawn_mainforce", + type = "button", + caption="Use Default Spawn"} + respawnGui.add{name = "respawn_mainforce_lbl1", type = "label", + caption="This will join the default team."} + respawnGui.add{name = "respawn_mainforce_lbl2", type = "label", + caption="If you are on another team all your research will be lost!"} + respawnGui.add{name = "respawn_mainforce_lbl3", type = "label", + caption="You will spawn at the default spawn point in the center."} + respawnGui.add{name = "respawn_mainforce_spacer", type = "label", + caption=" "} + ApplyStyle(respawnGui.respawn_mainforce_lbl1, my_label_style) + ApplyStyle(respawnGui.respawn_mainforce_lbl2, my_warning_style) + ApplyStyle(respawnGui.respawn_mainforce_lbl3, my_label_style) + ApplyStyle(respawnGui.respawn_mainforce_spacer, my_label_style) + + + respawnGui.add{name = "respawn_custom_spawn", + type = "button", + caption="Custom Spawn"} + respawnGui.add{name = "respawn_custom_lbl1", type = "label", + caption="This will join the default team."} + respawnGui.add{name = "respawn_custom_lbl2", type = "label", + caption="If you are on another team all your research will be lost!"} + respawnGui.add{name = "respawn_custom_lbl3", type = "label", + caption="You will spawn at your previous custom spawn point."} + respawnGui.add{name = "respawn_custom_spacer", type = "label", + caption=" "} + ApplyStyle(respawnGui.respawn_custom_lbl1, my_label_style) + ApplyStyle(respawnGui.respawn_custom_lbl2, my_warning_style) + ApplyStyle(respawnGui.respawn_custom_lbl3, my_label_style) + ApplyStyle(respawnGui.respawn_custom_spacer, my_label_style) + + if (ENABLE_OTHER_TEAMS) then + respawnGui.add{name = "respawn_custom_team", + type = "button", + caption="Custom Team Spawn"} + respawnGui.add{name = "respawn_custom_team_lbl1", type = "label", + caption="This will join your own custom team."} + respawnGui.add{name = "respawn_custom_team_lbl2", type = "label", + caption="You will have your own map vision and research tree. Use /s to talk to others."} + respawnGui.add{name = "respawn_custom_team_lbl3", type = "label", + caption="You will spawn at your previous custom spawn point."} + respawnGui.add{name = "respawn_custom_team_spacer", type = "label", + caption=" "} + ApplyStyle(respawnGui.respawn_custom_team_lbl1, my_label_style) + ApplyStyle(respawnGui.respawn_custom_team_lbl2, my_warning_style) + ApplyStyle(respawnGui.respawn_custom_team_lbl3, my_label_style) + ApplyStyle(respawnGui.respawn_custom_team_spacer, my_label_style) + end + + if (global.enableRespawnSurprise == true) then + respawnGui.add{name = "respawn_surpise", + type = "button", + caption="Surprise me!"} + end + + respawnGui.add{name = "respawn_note1", type = "label", + caption="You cannot generate new custom spawn points."} + ApplyStyle(respawnGui.respawn_note1, my_note_style) +end + +function GenerateStartingResources(player) + local surface = player.surface + + -- Generate stone + local stonePos = {x=player.position.x-25, + y=player.position.y-31} + + -- Generate coal + local coalPos = {x=player.position.x-25, + y=player.position.y-16} + + -- Generate copper ore + local copperOrePos = {x=player.position.x-25, + y=player.position.y+0} + + -- Generate iron ore + local ironOrePos = {x=player.position.x-25, + y=player.position.y+15} + + -- Tree generation is taken care of in chunk generation + + -- Generate oil patches + surface.create_entity({name="crude-oil", amount=START_OIL_AMOUNT, + position={player.position.x-30, player.position.y-2}}) + surface.create_entity({name="crude-oil", amount=START_OIL_AMOUNT, + position={player.position.x-30, player.position.y+2}}) + + for y=0, 15 do + for x=0, 15 do + if ((x-7)^2 + (y - 7)^2 < 7^2) then + surface.create_entity({name="iron-ore", amount=START_IRON_AMOUNT, + position={ironOrePos.x+x, ironOrePos.y+y}}) + surface.create_entity({name="copper-ore", amount=START_COPPER_AMOUNT, + position={copperOrePos.x+x, copperOrePos.y+y}}) + surface.create_entity({name="stone", amount=START_STONE_AMOUNT, + position={stonePos.x+x, stonePos.y+y}}) + surface.create_entity({name="coal", amount=START_COAL_AMOUNT, + position={coalPos.x+x, coalPos.y+y}}) + end + end + end +end + +function DoesPlayerHaveCustomSpawn(player) + for name,spawnPos in pairs(global.playerSpawns) do + if (player.name == name) then + return true + end + end + return false +end + +function DoesPlayerHaveActiveCustomSpawn(player) + if (DoesPlayerHaveCustomSpawn(player)) then + return global.activePlayerSpawns[player.name] + else + return false + end +end + +function ActivatePlayerCustomSpawn(player, value) + for name,_ in pairs(global.playerSpawns) do + if (player.name == name) then + global.activePlayerSpawns[player.name] = value + break + end + end +end + +function CreatePlayerCustomForce(player) + local newForce = nil + + -- Check if force already exists + if (game.forces[player.name] ~= nil) then + return game.forces[player.name] + + -- Create a new force using the player's name + elseif (TableLength(game.forces) < MAX_FORCES) then + newForce = game.create_force(player.name) + player.force = newForce + SetCeaseFireBetweenAllForces() + else + player.force = MAIN_FORCE + player.print("Sorry, no new teams can be created. You were assigned to the default team instead.") + end + + return newForce +end + +function SendPlayerToNewSpawnAndCreateIt(player, spawn) + -- Send the player to that position + player.teleport(spawn) + GivePlayerStarterItems(player) + + -- If we get a valid spawn point, setup the area + if (spawn ~= {x=0,y=0}) then + GenerateStartingResources(player) + ClearNearbyEnemies(player, SAFE_AREA_TILE_DIST) + end +end + +function SendPlayerToActiveSpawn(player) + if (DoesPlayerHaveActiveCustomSpawn(player)) then + player.teleport(global.playerSpawns[player.name]) + else + player.teleport(game.forces[MAIN_FORCE].get_spawn_position("nauvis")) + end +end + +function SendPlayerToRandomSpawn(player) + local numSpawns = TableLength(global.playerSpawns) + local rndSpawn = math.random(0,numSpawns) + local counter = 0 + + if (rndSpawn == 0) then + player.teleport(game.forces[MAIN_FORCE].get_spawn_position("nauvis")) + else + counter = counter + 1 + for name,spawnPos in pairs(global.playerSpawns) do + if (counter == rndSpawn) then + player.teleport(spawnPos) + break + end + counter = counter + 1 + end + end +end + + +-------------------------------------------------------------------------------- +-- Register event functions +-- These must be placed after the functions that are referenced! +-------------------------------------------------------------------------------- + +if ENABLE_SEPARATE_SPAWNS then + Event.register(defines.events.on_player_created, PlayerCreated) + Event.register(defines.events.on_player_respawned, PlayerRespawned) + Event.register(defines.events.on_gui_click, SpawnGuiClick) + Event.register(defines.events.on_chunk_generated, GenerateChunk) +end \ No newline at end of file diff --git a/tag.lua b/tag.lua new file mode 100644 index 0000000..025be7c --- /dev/null +++ b/tag.lua @@ -0,0 +1,59 @@ +function create_tag_gui(event) + local player = game.players[event.player_index] + if player.gui.top.tag == nil then + player.gui.top.add{name="tag", type="button", caption="Tag"} + end +end + +-- Tag list +local roles = { + {display_name = "[Solo]"}, + {display_name = "[Mining]"}, + {display_name = "[Power]"}, + {display_name = "[Oil]"}, + {display_name = "[Smelt]"}, + {display_name = "[Rail]"}, + {display_name = "[Defense]"}, + {display_name = "[Circuits]"}, + {display_name = "[Labs]"}, + {display_name = "[Logistics]"}, + {display_name = "[Misc]"}, + {display_name = "[Aliens]"}, + {display_name = "[Rocket]"}, + {display_name = "[AFK]"}, + {display_name = "Clear"}} + +function expand_tag_gui(player) + local frame = player.gui.left["tag-panel"] + if (frame) then + frame.destroy() + else + local frame = player.gui.left.add{type="frame", name="tag-panel", caption="What are you doing:"} + for _, role in pairs(roles) do + frame.add{type="button", caption=role.display_name, name=role.display_name} + end + end +end + +local function on_gui_click(event) + if not (event and event.element and event.element.valid) then return end + local player = game.players[event.element.player_index] + local name = event.element.name + + if (name == "tag") then + expand_tag_gui(player) + end + + if (name == "Clear") then + player.tag = "" + return + end + for _, role in pairs(roles) do + if (name == role.display_name) then + player.tag = role.display_name end + end +end + + +Event.register(defines.events.on_gui_click, on_gui_click) +Event.register(defines.events.on_player_joined_game, create_tag_gui) \ No newline at end of file