You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2022 lines
78 KiB

local Zone = {}
-- constants
Zone.solar_multiplier = 1
Zone.discovery_scan_radius = 32
Zone.clear_enemies_radius = 512
Zone.travel_cost_interstellar = 400 -- stellar position distance, roughly 50 distance between stars, can be up to 300 apart
Zone.travel_cost_star_gravity = 500 -- roughly 10-20 base for a star
Zone.travel_cost_planet_gravity = 100 -- roughly 10-20 base for a planet
Zone.travel_cost_space_distortion = Zone.travel_cost_interstellar * 25 -- based on 0-1 range
Zone.name_tech_discover_random = mod_prefix.."zone-discovery-random"
Zone.name_tech_discover_targeted = mod_prefix.."zone-discovery-targeted"
Zone.name_tech_discover_deep = mod_prefix.."zone-discovery-deep"
-- based on alien biomes, unsure as to how ti make this more dynamic
-- biome name = {tilenames}
Zone.biome_tiles = {
["out-of-map"] = {"out-of-map"}, -- always allowed
["water"] = {"water", "deepwater", "water-green", "deepwater-green", "water-shallow", "water-mud"},
["dirt-purple"] = {"mineral-purple-dirt-1", "mineral-purple-dirt-2", "mineral-purple-dirt-3", "mineral-purple-dirt-4", "mineral-purple-dirt-5", "mineral-purple-dirt-6"},
["dirt-violet"] = {"mineral-violet-dirt-1", "mineral-violet-dirt-2", "mineral-violet-dirt-3", "mineral-violet-dirt-4", "mineral-violet-dirt-5", "mineral-violet-dirt-6"},
["dirt-red"] = {"mineral-red-dirt-1", "mineral-red-dirt-2", "mineral-red-dirt-3", "mineral-red-dirt-4", "mineral-red-dirt-5", "mineral-red-dirt-6"},
["dirt-brown"] = {"mineral-brown-dirt-1", "mineral-brown-dirt-2", "mineral-brown-dirt-3", "mineral-brown-dirt-4", "mineral-brown-dirt-5", "mineral-brown-dirt-6"},
["dirt-tan"] = {"mineral-tan-dirt-1", "mineral-tan-dirt-2", "mineral-tan-dirt-3", "mineral-tan-dirt-4", "mineral-tan-dirt-5", "mineral-tan-dirt-6"},
["dirt-aubergine"] = {"mineral-aubergine-dirt-1", "mineral-aubergine-dirt-2", "mineral-aubergine-dirt-3", "mineral-aubergine-dirt-4", "mineral-aubergine-dirt-5", "mineral-aubergine-dirt-6"},
["dirt-dustyrose"] = {"mineral-dustyrose-dirt-1", "mineral-dustyrose-dirt-2", "mineral-dustyrose-dirt-3", "mineral-dustyrose-dirt-4", "mineral-dustyrose-dirt-5", "mineral-dustyrose-dirt-6"},
["dirt-beige"] = {"mineral-beige-dirt-1", "mineral-beige-dirt-2", "mineral-beige-dirt-3", "mineral-beige-dirt-4", "mineral-beige-dirt-5", "mineral-beige-dirt-6"},
["dirt-cream"] = {"mineral-cream-dirt-1", "mineral-cream-dirt-2", "mineral-cream-dirt-3", "mineral-cream-dirt-4", "mineral-cream-dirt-5", "mineral-cream-dirt-6"},
["dirt-black"] = {"mineral-black-dirt-1", "mineral-black-dirt-2", "mineral-black-dirt-3", "mineral-black-dirt-4", "mineral-black-dirt-5", "mineral-black-dirt-6"},
["dirt-grey"] = {"mineral-grey-dirt-1", "mineral-grey-dirt-2", "mineral-grey-dirt-3", "mineral-grey-dirt-4", "mineral-grey-dirt-5", "mineral-grey-dirt-6"},
["dirt-white"] = {"mineral-white-dirt-1", "mineral-white-dirt-2", "mineral-white-dirt-3", "mineral-white-dirt-4", "mineral-white-dirt-5", "mineral-white-dirt-6"},
["sand-purple"] = {"mineral-purple-sand-1", "mineral-purple-sand-2", "mineral-purple-sand-3"},
["sand-violet"] = {"mineral-violet-sand-1", "mineral-violet-sand-2", "mineral-violet-sand-3"},
["sand-red"] = {"mineral-red-sand-1", "mineral-red-sand-2", "mineral-red-sand-3"},
["sand-brown"] = {"mineral-brown-sand-1", "mineral-brown-sand-2", "mineral-brown-sand-3"},
["sand-tan"] = {"mineral-tan-sand-1", "mineral-tan-sand-2", "mineral-tan-sand-3"},
["sand-aubergine"] = {"mineral-aubergine-sand-1", "mineral-aubergine-sand-2", "mineral-aubergine-sand-3"},
["sand-dustyrose"] = {"mineral-dustyrose-sand-1", "mineral-dustyrose-sand-2", "mineral-dustyrose-sand-3"},
["sand-beige"] = {"mineral-beige-sand-1", "mineral-beige-sand-2", "mineral-beige-sand-3"},
["sand-cream"] = {"mineral-cream-sand-1", "mineral-cream-sand-2", "mineral-cream-sand-3"},
["sand-black"] = {"mineral-black-sand-1", "mineral-black-sand-2", "mineral-black-sand-3"},
["sand-grey"] = {"mineral-grey-sand-1", "mineral-grey-sand-2", "mineral-grey-sand-3"},
["sand-white"] = {"mineral-white-sand-1", "mineral-white-sand-2", "mineral-white-sand-3"},
["vegetation-green"] = {"vegetation-green-grass-1", "vegetation-green-grass-2", "vegetation-green-grass-3", "vegetation-green-grass-4"},
["vegetation-olive"] = {"vegetation-olive-grass-1", "vegetation-olive-grass-2"},
["vegetation-yellow"] = {"vegetation-yellow-grass-1", "vegetation-yellow-grass-2"},
["vegetation-orange"] = {"vegetation-orange-grass-1", "vegetation-orange-grass-2"},
["vegetation-red"] = {"vegetation-red-grass-1", "vegetation-red-grass-2"},
["vegetation-violet"] = {"vegetation-violet-grass-1", "vegetation-violet-grass-2"},
["vegetation-purple"] = {"vegetation-purple-grass-1", "vegetation-purple-grass-2"},
["vegetation-mauve"] = {"vegetation-mauve-grass-1", "vegetation-mauve-grass-2"},
["vegetation-blue"] = {"vegetation-blue-grass-1", "vegetation-blue-grass-2"},
["vegetation-turquoise"] = {"vegetation-turquoise-grass-1", "vegetation-turquoise-grass-2"},
["volcanic-orange"] = {"volcanic-orange-heat-1", "volcanic-orange-heat-2", "volcanic-orange-heat-3", "volcanic-orange-heat-4"},
["volcanic-green"] = {"volcanic-green-heat-1", "volcanic-green-heat-2", "volcanic-green-heat-3", "volcanic-green-heat-4"},
["volcanic-blue"] = {"volcanic-blue-heat-1", "volcanic-blue-heat-2", "volcanic-blue-heat-3", "volcanic-blue-heat-4"},
["volcanic-purple"] = {"volcanic-purple-heat-1", "volcanic-purple-heat-2", "volcanic-purple-heat-3", "volcanic-purple-heat-4"},
["frozen-snow"] = {"frozen-snow-0", "frozen-snow-1", "frozen-snow-2", "frozen-snow-3", "frozen-snow-4"},
["frozen-ice"] = {"frozen-snow-5", "frozen-snow-6", "frozen-snow-7", "frozen-snow-8", "frozen-snow-9"},
}
Zone.biome_collections = {
["all-sand"] = {"sand-purple", "sand-violet", "sand-red", "sand-brown", "sand-tan", "sand-aubergine", "sand-dustyrose", "sand-beige", "sand-cream", "sand-black", "sand-grey", "sand-white"},
["all-dirt"] = {"dirt-purple", "dirt-violet", "dirt-red", "dirt-brown", "dirt-tan", "dirt-aubergine", "dirt-dustyrose", "dirt-beige", "dirt-cream", "dirt-black", "dirt-grey", "dirt-white"},
["all-vegetation"] = {"vegetation-green", "vegetation-olive", "vegetation-yellow", "vegetation-orange", "vegetation-red",
"vegetation-violet", "vegetation-purple", "vegetation-mauve", "vegetation-blue", "vegetation-turquoise" },
["all-volcanic"] = {"volcanic-orange", "volcanic-green", "volcanic-blue", "volcanic-purple"},
["all-frozen"] = {"frozen-snow", "frozen-ice"},
}
Zone.signal_to_zone_type = {
[mod_prefix.."planet"] = "planet",
[mod_prefix.."moon"] = "moon",
[mod_prefix.."planet-orbit"] = "orbit",
[mod_prefix.."moon-orbit"] = "orbit",
[mod_prefix.."star"] = "orbit",
[mod_prefix.."asteroid-belt"] = "asteroid-belt",
[mod_prefix.."asteroid-field"] = "asteroid-field",
[mod_prefix.."anomaly"] = "anomaly",
}
Zone.controls_without_frequency_multiplier = {
"trees",
"enemy-base"
}
-- NOTE: cliff and base terrain sliders have special settings,
--[[ eg:
biome_replacements = {
{replace={"all-dirt", "all-sand", "all-volcanic"}, with="sand-red"},
{replace={"all-vegetation", "all-frozen"}, with="vegetation-red"}
} ]]--
function Zone.get_default()
return Zone.from_name("Nauvis")
end
function Zone.get_force_home_zone(force_name)
if global.forces[force_name] and global.forces[force_name].homeworld_index then
return Zone.from_zone_index(global.forces[force_name].homeworld_index)
end
end
function Zone.type_title(zone)
if zone.type == "planet" then
return "Planet"
elseif zone.type == "moon" then
return "Moon"
elseif zone.type == "star" then
return "Star"
elseif zone.type == "asteroid-field" then
return "Asteroid Field"
elseif zone.type == "asteroid-belt" then
return "Asteroid Belt"
elseif zone.type == "anomaly" then
return "Anomaly"
elseif zone.type == "spaceship" then
return "Spaceship"
elseif zone.type == "orbit" then
return (Zone.type_title(zone.parent) .. " Orbit")
end
end
function Zone.get_signal_name(zone)
-- used for rich text
if zone.type == "orbit" and zone.parent.type == "star" then
return mod_prefix.."star"
elseif zone.type == "orbit" and zone.parent.type == "planet" then
return mod_prefix.."planet-orbit"
elseif zone.type == "orbit" and zone.parent.type == "moon" then
return mod_prefix.."moon-orbit"
else
return mod_prefix..zone.type
end
end
function Zone.get_icon(zone)
-- used for rich text
return "virtual-signal/" .. Zone.get_signal_name(zone)
end
function Zone.is_solid(zone)
return zone.type == "planet" or zone.type == "moon"
end
function Zone.is_space(zone)
return not Zone.is_solid(zone)
end
function Zone.from_zone_index(zone_index)
return global.zone_index[zone_index]
end
function Zone.from_name(name)
return global.zones_by_name[name]
end
function Zone.from_surface_index(surface_index)
return global.zones_by_surface[surface_index]
end
function Zone.from_surface(surface)
local from_index = Zone.from_surface_index(surface.index)
if from_index then return from_index end
-- maybe a spaceship
return Spaceship.from_own_surface_index(surface.index)
end
function Zone.get_stellar_position(zone)
if not zone then return nil end
if zone.type == "anomaly" then return {x = 0, y = 0} end
-- everything else should have a stellar position
return zone.stellar_position or Zone.get_stellar_position(zone.parent)
end
function Zone.get_star_gravity_well(zone)
if zone.type == "orbit" then
return Zone.get_star_gravity_well(zone.parent)
end
return zone.star_gravity_well or 0
end
function Zone.get_planet_gravity_well(zone)
if zone.type == "orbit" then
if zone.parent.type == "star" then
return 0
elseif zone.parent.type == "planet" then
return Zone.get_planet_gravity_well(zone.parent) - 1
else
return Zone.get_planet_gravity_well(zone.parent) - 0.5
end
end
return zone.planet_gravity_well or 0
end
function Zone.get_space_distortion(zone) -- anomaly
if zone.space_distortion then
return zone.space_distortion
end
return zone.type == "anomaly" and 1 or 0
end
function Zone.find_parent_star(zone)
if zone.type == "star" then
return zone
elseif zone.parent then
return Zone.find_parent_star(zone.parent)
end
end
function Zone.apply_markers(zone)
for force_name, force_data in pairs(global.forces) do
local force = game.forces[force_name]
if force and force_data.zones_discovered[zone.index] then
if not (force_data.zones_discovered[zone.index].marker and force_data.zones_discovered[zone.index].marker.valid) then
local surface = Zone.get_surface(zone)
if surface then
force_data.zones_discovered[zone.index].marker = force.add_chart_tag(surface, {
icon = {type = "virtual", name = Zone.get_signal_name(zone)},
position = {0,0},
text = zone.name
})
end
end
end
end
end
function Zone.validate_controls_and_error(controls)
if controls then
for name, control in pairs(controls) do
if type(name) ~= "string" then
error(serpent.block(name))
end
end
end
end
function Zone.validate_controls(controls)
if controls then
for name, control in pairs(controls) do
if type(name) ~= "string" then
controls[name] = nil
end
end
end
end
function Zone.apply_controls_to_mapgen(zone, controls, mapgen)
Zone.validate_controls(controls)
local frequency_multiplier = Zone.get_frequency_multiplier(zone)
for name, control in pairs(controls) do
if type(name) == "string" then
if name == "moisture" then
mapgen.property_expression_names = mapgen.property_expression_names or {}
if control.frequency then
mapgen.property_expression_names["control-setting:moisture:frequency:multiplier"] = control.frequency
end
if control.bias then
mapgen.property_expression_names["control-setting:moisture:bias"] = control.bias
end
elseif name == "aux" then
mapgen.property_expression_names = mapgen.property_expression_names or {}
if control.frequency then
mapgen.property_expression_names["control-setting:aux:frequency:multiplier"] = control.frequency
end
if control.bias then
mapgen.property_expression_names["control-setting:aux:bias"] = control.bias
end
elseif name == "water" then
if control.frequency then
mapgen.terrain_segmentation = control.frequency
end
if control.size then
mapgen.water = control.size
end
elseif name == "cliff" then
mapgen.cliff_settings = mapgen.cliff_settings or {
name="cliff",
cliff_elevation_0=10, -- default
cliff_elevation_interval=400, -- when set from the GUI the value is 40 / frequency.
richness=0, -- 0.17 to 6.
}
if control.frequency then
mapgen.cliff_settings.cliff_elevation_interval = 40 / control.frequency
mapgen.cliff_settings.cliff_elevation_0 = mapgen.cliff_settings.cliff_elevation_interval / 4
end
if control.richness then
mapgen.cliff_settings.richness = richness
end
else
if game.autoplace_control_prototypes[name] then
mapgen.autoplace_controls[name] = table.deepcopy(control)
if mapgen.autoplace_controls[name].frequency and not Util.table_contains(Zone.controls_without_frequency_multiplier, name) then
mapgen.autoplace_controls[name].frequency = mapgen.autoplace_controls[name].frequency * frequency_multiplier
end
else
log("Zone.apply_controls_to_mapgen: Attempt to apply invalid control name to mapgen: "..name)
controls[name] = nil
end
end
end
end
end
function Zone.get_frequency_multiplier(zone)
if zone.radius then
return 5000 / zone.radius
end
return 1
end
function Zone.create_surface(zone)
if not zone.surface_index then
Universe.inflate_climate_controls(zone, false)
-- TODO planets should have customised controls
local map_gen_settings = table.deepcopy(game.default_map_gen_settings)
map_gen_settings.width = 0
map_gen_settings.height = 0
if not zone.seed then zone.seed = math.random(4294967295) end
map_gen_settings.seed = zone.seed
local autoplace_controls = map_gen_settings.autoplace_controls
zone.controls = zone.controls or {}
local frequency_multiplier = Zone.get_frequency_multiplier(zone) -- increase for small planets and moons
-- For all possible controls set values so it can be regenreated consistently.
for control_name, control_prototype in pairs(game.autoplace_control_prototypes) do
if control_name ~= "planet-size" and game.autoplace_control_prototypes[control_name] then
zone.controls[control_name] = zone.controls[control_name] or {}
zone.controls[control_name].frequency = (zone.controls[control_name].frequency or (0.17 + math.random() * math.random() * 2))
zone.controls[control_name].size = zone.controls[control_name].size or (0.1 + math.random() * 0.8)
zone.controls[control_name].richness = zone.controls[control_name].richness or (0.1 + math.random() * 0.8)
end
end
zone.controls.moisture = zone.controls.moisture or {}
zone.controls.moisture.frequency = (zone.controls.moisture.frequency or (0.17 + math.random() * math.random() * 2))
zone.controls.moisture.bias = zone.controls.moisture.bias or (math.random() - 0.5)
zone.controls.aux = zone.controls.aux or {}
zone.controls.aux.frequency = (zone.controls.aux.frequency or (0.17 + math.random() * math.random() * 2))
zone.controls.aux.bias = zone.controls.aux.bias or (math.random() - 0.5)
zone.controls.cliff = zone.controls.cliff or {}
zone.controls.cliff.frequency = (zone.controls.cliff.frequency or (0.17 + math.random() * math.random() * 10)) --
zone.controls.cliff.richness = zone.controls.cliff.richness or (math.random() * 1.25)
--[[
map_gen_settings.property_expression_names = {
["control-setting:moisture:frequency:multiplier"] = zone.controls.moisture.frequency * frequency_multiplier,
["control-setting:moisture:bias"] = zone.controls.moisture.bias,
["control-setting:aux:frequency:multiplier"] = zone.controls.aux.frequency * frequency_multiplier,
["control-setting:aux:bias"] = zone.controls.aux.bias,
}
zone.controls.water = zone.controls.water or {}
zone.controls.water.size = zone.controls.water.size or 0.01 -- high is 6, low is 0
zone.controls.water.frequency = (zone.controls.water.frequency or 1) -- low is 0.17
zone.controls.water.frequency = 0.5 + zone.controls.water.frequency / 2
map_gen_settings.water = zone.controls.water.size
map_gen_settings.terrain_segmentation = zone.controls.water.frequency
]]--
Zone.apply_controls_to_mapgen(zone, zone.controls, map_gen_settings)
autoplace_controls["planet-size"] = { frequency = 1, size = 1 } -- default
-- planet_radius = 10000 / 6 * (6 + log(1/planet_frequency/6, 2))
-- planet_frequency = 1 / 6 / 2 ^ (planet_radius * 6 / 10000 - 6)
local planet_size_frequency = 1/6 -- 10000 radius planet
if Zone.is_solid(zone) then
map_gen_settings.width = zone.radius*2+32
map_gen_settings.height = zone.radius*2+32
-- planet or moon
--planet_size_frequency = 1 / (zone.radius / 10000)
planet_size_frequency = 1 / 6 / 2 ^ (zone.radius * 6 / 10000 - 6)
local penalty = -100000
if zone.tags and util.table_contains(zone.tags, "water_none") then
map_gen_settings.property_expression_names["tile:deepwater:probability"] = penalty
map_gen_settings.property_expression_names["tile:water:probability"] = penalty
map_gen_settings.property_expression_names["tile:water-shallow:probability"] = penalty
map_gen_settings.property_expression_names["tile:water-mud:probability"] = penalty
end
map_gen_settings.property_expression_names["decorative:se-crater3-huge:probability"] = penalty
if not zone.is_homeworld then
map_gen_settings.starting_area = 0.5
end
else
if zone.type == "orbit" then
autoplace_controls["planet-size"].size = zone.parent.radius and (zone.parent.radius / 200) or 50
elseif zone.type == "asteroid-belt" then
autoplace_controls["planet-size"].size = 200
elseif zone.type == "asteroid-field" then
autoplace_controls["planet-size"].size = 10000
end
planet_size_frequency = 1/1000
map_gen_settings.cliff_settings={
name="cliff",
cliff_elevation_0=10, -- default
cliff_elevation_interval=400, -- when set from the GUI the value is 40 / frequency.
richness=0, -- 0.17 to 6.
}
zone.controls.cliff.frequency = 0
zone.controls.cliff.richness = 0
map_gen_settings.property_expression_names = {
["control-setting:moisture:frequency:multiplier"] = 10,
["control-setting:moisture:bias"] = -1,
["control-setting:aux:frequency:multiplier"] = 0,
["control-setting:aux:bias"] = 0,
}
map_gen_settings.starting_area = 0
end
autoplace_controls["planet-size"].frequency = planet_size_frequency
Log.debug_log("Creating surface " .. zone.name .. " with map_gen_settings:")
Log.debug_log(util.table_to_string(map_gen_settings))
if Zone.is_space(zone) then
-- Speed up terrain generation by excluding everything not specifically allowed to spawn
--map_gen_settings.default_enable_all_autoplace_controls = false
map_gen_settings.autoplace_settings={
["decorative"]={
treat_missing_as_default=false,
settings={
["se-crater3-huge"] ={},
["se-crater1-large-rare"] ={},
["se-crater1-large"] ={},
["se-crater2-medium"] ={},
["se-crater4-small"] ={},
["se-sand-decal-space"] ={},
["se-stone-decal-space"] ={},
["se-rock-medium-asteroid"] ={},
["se-rock-small-asteroid"] ={},
["se-rock-tiny-asteroid"] ={},
["se-sand-rock-medium-asteroid"] ={},
["se-sand-rock-small-asteroid"] ={}
}
},
--[[["entity"]={
treat_missing_as_default=false,
settings={
["se-rock-huge-asteroid"] ={},
["se-rock-big-asteroid"] ={},
["se-sand-rock-big-asteroid"] ={},
["se-rock-huge-space"] ={},
["se-rock-big-space"] ={},
}
},]]--
["tile"]={
treat_missing_as_default=false,
settings={
["se-asteroid"]={},
["se-space"]={}
}
},
}
else
-- speed up terrain generation by specifying specific things not to spawn
local penalty = -100000
map_gen_settings.property_expression_names["decorative:se-crater3-huge:probability"] = penalty
map_gen_settings.property_expression_names["decorative:se-crater1-large-rare:probability"] = penalty
map_gen_settings.property_expression_names["decorative:se-crater1-large:probability"] = penalty
map_gen_settings.property_expression_names["decorative:se-crater2-medium:probability"] = penalty
map_gen_settings.property_expression_names["decorative:se-crater4-small:probability"] = penalty
map_gen_settings.property_expression_names["decorative:se-sand-decal-space:probability"] = penalty
map_gen_settings.property_expression_names["decorative:se-stone-decal-space:probability"] = penalty
map_gen_settings.property_expression_names["decorative:se-rock-medium-asteroid:probability"] = penalty
map_gen_settings.property_expression_names["decorative:se-rock-small-asteroid:probability"] = penalty
map_gen_settings.property_expression_names["decorative:se-rock-tiny-asteroid:probability"] = penalty
map_gen_settings.property_expression_names["decorative:se-sand-rock-medium-asteroid:probability"] = penalty
map_gen_settings.property_expression_names["decorative:se-sand-rock-small-asteroid:probability"] = penalty
map_gen_settings.property_expression_names["entity:se-rock-huge-asteroid:probability"] = penalty
map_gen_settings.property_expression_names["entity:se-rock-big-asteroid:probability"] = penalty
map_gen_settings.property_expression_names["entity:se-sand-rock-big-asteroid:probability"] = penalty
map_gen_settings.property_expression_names["entity:se-rock-huge-space:probability"] = penalty
map_gen_settings.property_expression_names["entity:se-rock-big-space:probability"] = penalty
map_gen_settings.property_expression_names["tile:se-asteroid:probability"] = penalty
map_gen_settings.property_expression_names["tile:se-space:probability"] = penalty
end
local surface
if not game.surfaces[zone.name] then
surface = game.create_surface(zone.name, map_gen_settings)
surface.force_generate_chunk_requests()
else
-- this happens if the mod was uninstalled and reinstalled. The surface will be invalid and unfaxable.
-- game.delete_surface(zone.name) -- does not work in time to re make the surface
surface = game.surfaces[zone.name]
surface.clear(false)
end
zone.surface_index = surface.index
global.zones_by_surface = global.zones_by_surface or {}
global.zones_by_surface[surface.index] = zone
Zone.set_solar_and_daytime(zone)
if zone.type == "planet" and not zone.is_homeworld then
-- Vault planet
if (not zone.ruins) and not zone.glyph then
Ancient.assign_zone_next_glyph(zone)
end
Ancient.make_vault_exterior(zone) -- only makes the vault if glyph exists
-- Interburbulator planet
if (not zone.glyph) and (not zone.ruins) and not global.interburbulator then
Interburbulator.make_interburbulator(zone)
end
if zone.interburbulator then
Interburbulator.build_platform()
end
end
if not zone.is_homeworld then
-- Unique ruins
Ruin.zone_assign_unique_ruins(zone)
Ruin.zone_build_ruins(zone, surface)
end
if zone.type == "anomaly" then
Ancient.make_gate(Ancient.gate_default_position)
Ruin.build({ruin_name = "galaxy-ship", surface_index = surface.index, position = Ancient.galaxy_ship_default_position})
end
end
end
function Zone.get_star_from_child(zone)
if zone.type == "star" then
return zone
elseif zone.parent then
return Zone.get_star_from_child(zone.parent)
end
end
function Zone.get_stellar_object_from_child(zone)
if zone.type == "asteroid-field" then
return zone
else
return Zone.get_star_from_child(zone)
end
end
function Zone.get_star_from_position(zone)
if not zone.stellar_position then return end
for _, star in pairs(global.universe.stars) do
if star.stellar_position.x == zone.stellar_position.x and star.stellar_position.y == zone.stellar_position.y then
return star
end
end
end
function Zone.get_stellar_object_from_position(zone)
local star = Zone.get_star_from_position(zone)
if star then return star end
for _, asteroid_field in pairs(global.universe.space_zones) do
if asteroid_field.stellar_position.x == zone.stellar_position.x and asteroid_field.stellar_position.y == zone.stellar_position.y then
return asteroid_field
end
end
end
function Zone.get_solar(zone) -- can be spaceship too
-- return the actual expected light % including daytime for space zones and spaceships.
-- return peak expected light % space solid zones.
if zone.type == "anomaly" then
return 0
end
local star
local star_gravity_well = 0
if zone.type == "spaceship" then
star = zone.near_star
star_gravity_well = zone.star_gravity_well or 0
else
star = Zone.get_star_from_child(zone)
star_gravity_well = Zone.get_star_gravity_well(zone)
end
local light_percent = 0
if star then
light_percent = 1.6 * star_gravity_well / (star.star_gravity_well + 1)
end
if Zone.is_space(zone) then
if(zone.type == "orbit" and zone.parent and zone.parent.type == "star") then -- star
light_percent = light_percent * 10 -- x20
elseif zone.type == "asteroid-belt" then
light_percent = light_percent * 2.5 -- x5
else
light_percent = light_percent * 5 -- x10
if zone.parent and zone.parent.radius then
light_percent = light_percent * (1 - 0.1 * zone.parent.radius / 10000)
end
end
light_percent = light_percent + 0.01
else
if zone.radius then
light_percent = light_percent * (1 - 0.1 * zone.radius / 10000)
if zone.is_homeworld then
light_percent = 1
end
end
end
if zone.space_distortion and zone.space_distortion > 0 then
light_percent = light_percent * (1 - zone.space_distortion)
if zone.is_homeworld then
light_percent = 1
end
end
return light_percent
end
function Zone.get_display_light_percent(zone)
return Zone.get_solar(zone)
end
function Zone.set_solar_and_daytime(zone)
local surface = Zone.get_surface(zone)
if not surface then return end
if zone.type == "anomaly" then
surface.solar_power_multiplier = 0
surface.daytime = 0.5
return
end
local light_percent = Zone.get_solar(zone)
if Zone.is_space(zone) then
-- light_percent is the total output
-- but we have most space zones daylight ranging from mid evening to night so that lights are active.
-- so that is the main driving factor.
-- except stars.
surface.freeze_daytime = true
if zone.type == "orbit" and zone.parent.type == "star" then
surface.daytime = 0 -- very bright
surface.solar_power_multiplier = Zone.solar_multiplier * light_percent
else
if light_percent >= 0.5 then
surface.daytime = 0.35 -- half light
surface.solar_power_multiplier = Zone.solar_multiplier * light_percent * 2 -- x2 compensate for half light
else
-- for now all zone surfaces are syneced so that daytime 0 is at game.tick 0
surface.daytime = 0.45 - 0.2 * light_percent
surface.solar_power_multiplier = Zone.solar_multiplier -- x2 compensate for half light max
-- light_percent of 1 would be 0.35 (half-light),
-- light_percent of 0 would be 0.45 (dark)
end
end
else
-- planet or moon
-- has daytime
surface.daytime = (game.tick / zone.ticks_per_day) % 1
surface.solar_power_multiplier = Zone.solar_multiplier * light_percent
if zone.ticks_per_day then
surface.ticks_per_day = zone.ticks_per_day
end
end
if zone.type == "anomaly" then
surface.solar_power_multiplier = 0
surface.daytime = 0.5
end
end
function Zone.get_flags_weight(zone, force_name, playerdata)
local weight = (
((playerdata.track_glyphs and zone.glyph) and 0.96 or 0)
+ ((zone.interburbulator or zone.ruins) and 0.95 or 0)
+ ((playerdata.visited_zone and playerdata.visited_zone[zone.index]) and 0.97 or 0)
+ ((global.forces[force_name].zone_assets and global.forces[force_name].zone_assets[zone.index] and table_size(global.forces[force_name].zone_assets[zone.index].rocket_landing_pad_names) > 0) and 0.98 or 0)
+ ((global.forces[force_name].zone_assets and global.forces[force_name].zone_assets[zone.index] and table_size(global.forces[force_name].zone_assets[zone.index].rocket_launch_pad_names) > 0) and 1 or 0)
)
Log.debug_log("Zone.get_flags_weight " .. zone.name.." " ..weight, "zone")
return weight
end
function Zone.get_surface(zone) -- returns surface but does not build
if zone.type == "spaceship" then
return Spaceship.get_current_surface(zone)
end
if zone.surface_index then
return game.get_surface(zone.surface_index)
end
return nil
end
function Zone.get_surface_name(zone)
local surface = Zone.get_surface(zone)
if surface then return surface.name end
end
function Zone.get_make_surface(zone)
if zone.type == "spaceship" then
return Spaceship.get_current_surface(zone)
end
if not zone.surface_index then Zone.create_surface(zone) end
return game.get_surface(zone.surface_index)
end
function Zone.discover(force_name, zone, source) -- source could be "Satellite "
global.forces[force_name] = global.forces[force_name] or {}
global.forces[force_name].zones_discovered = global.forces[force_name].zones_discovered or {}
global.forces[force_name].zones_discovered_count = global.forces[force_name].zones_discovered_count or 0
if not global.forces[force_name].zones_discovered[zone.index] then
Universe.inflate_climate_controls(zone, false)
global.forces[force_name].zones_discovered[zone.index] = {
discovered_at = game.tick,
marker = nil
}
global.forces[force_name].zones_discovered_count = global.forces[force_name].zones_discovered_count + 1
if zone.type == "planet" and (not zone.is_homeworld) and (not zone.ruins) and (not zone.glyph) then
Ancient.assign_zone_next_glyph(zone)
end
-- Unique ruins
Ruin.zone_assign_unique_ruins(zone)
local message = nil
if source then
message = {"space-exploration.source-discovered-zone", source, Zone.type_title(zone), Zone.get_icon(zone), zone.name}
--game.forces[force_name].print(source .. " discovered a new " .. Zone.type_title(zone) .. ": " .. zone.name)
else
message = {"space-exploration.discovered-zone", source, Zone.type_title(zone), Zone.get_icon(zone), zone.name}
--game.forces[force_name].print("Discovered a new " .. Zone.type_title(zone) .. ": " .. zone.name)
end
local tick_task = new_tick_task("force-message")
tick_task.force_name = force_name
tick_task.message = message
Zone.apply_markers(zone) -- in case the surface exists
for _, player in pairs(game.connected_players) do
if Zonelist.get_main_window(player.index) then
Zonelist.gui_update_list(player.index)
end
end
if zone.type == "anomaly" then
local tick_task = new_tick_task("force-message")
tick_task.force_name = force_name
tick_task.message = {"space-exploration.discovered-anomaly-additional"}
tick_task.delay_until = game.tick + 300 --5s
end
if zone.glyph then
local force = game.forces[force_name]
for _, player in pairs(force.players) do
if player.connected then
local playerdata = get_make_playerdata(player)
if playerdata.track_glyphs then
player.print({"space-exploration.discovered-glyph-vault", zone.name})
end
end
end
end
return true
end
return false
end
function Zone.discover_next_research(force_name, source, allow_targeted)
global.forces[force_name] = global.forces[force_name] or {}
global.forces[force_name].zones_discovered = global.forces[force_name].zones_discovered or {}
global.forces[force_name].zones_discovered_count = global.forces[force_name].zones_discovered_count or 0
local target_resource = "n/a"
if allow_targeted then
target_resource = global.forces[force_name].search_for_resource
end
local can_discover = {}
local can_discover_targeted = {}
-- star and deep space discovery should be bias to nearer positions
local closest_1 = nil
local closest_stellar_distance = 1000000
local pos1 = {x = 0, y = 0} -- focus on aeras close to the center of the star map
for _, star in pairs(global.universe.stars) do
if not global.forces[force_name].zones_discovered[star.index] then
local pos2 = Zone.get_stellar_position(star)
local distance = util.vectors_delta_length(pos1, pos2)
if distance < closest_stellar_distance then
closest_stellar_distance = distance
closest_1 = star
end
end
end
if closest_1 then
-- x5
table.insert(can_discover, closest_1)
table.insert(can_discover, closest_1)
table.insert(can_discover, closest_1)
table.insert(can_discover, closest_1)
table.insert(can_discover, closest_1)
end
for _, star in pairs(global.universe.stars) do
if global.forces[force_name].zones_discovered[star.index] then
for _, planet in pairs(star.children) do
if not global.forces[force_name].zones_discovered[planet.index] then
if planet.primary_resource == target_resource then
table.insert(can_discover_targeted, planet)
if planet.type == "planet" then
--x5 bias towards planets
table.insert(can_discover_targeted, planet)
table.insert(can_discover_targeted, planet)
table.insert(can_discover_targeted, planet)
table.insert(can_discover_targeted, planet)
end
else
table.insert(can_discover, planet)
if planet.type == "planet" then
--x5 bias towards planets
table.insert(can_discover, planet)
table.insert(can_discover, planet)
table.insert(can_discover, planet)
table.insert(can_discover, planet)
end
end
else
if planet.children then
for _, moon in pairs(planet.children) do
if not global.forces[force_name].zones_discovered[moon.index] then
if moon.primary_resource == target_resource then
table.insert(can_discover_targeted, moon)
else
table.insert(can_discover, moon)
end
end
end
end
end
end
end
end
if #can_discover_targeted > 0 then
return Zone.discover(force_name, Util.random_from_array(can_discover_targeted), source)
end
if #can_discover > 0 then
return Zone.discover(force_name, Util.random_from_array(can_discover), source)
end
end
function Zone.discover_next_research_deep_space(force_name, source, allow_targeted)
global.forces[force_name] = global.forces[force_name] or {}
global.forces[force_name].zones_discovered = global.forces[force_name].zones_discovered or {}
global.forces[force_name].zones_discovered_count = global.forces[force_name].zones_discovered_count or 0
if game.forces[force_name].technologies[Zone.name_tech_discover_deep].level > 11 then
-- should have discovered multiple stars at this point
if not global.forces[force_name].zones_discovered[global.universe.anomaly.index] then
return Zone.discover(force_name, global.universe.anomaly, source)
end
end
local target_resource = "n/a"
if allow_targeted then
target_resource = global.forces[force_name].search_for_resource
end
local can_discover = {}
local can_discover_targeted = {}
for _, zone in pairs(global.universe.space_zones) do
if not global.forces[force_name].zones_discovered[zone.index] then
if zone.primary_resource == target_resource then
table.insert(can_discover_targeted, zone)
else
table.insert(can_discover, zone)
end
end
end
if #can_discover_targeted > 0 then
return Zone.discover(force_name, Util.random_from_array(can_discover_targeted), source)
end
if #can_discover > 0 then
return Zone.discover(force_name, Util.random_from_array(can_discover), source)
end
end
function Zone.discover_next_satellite(force_name, source, system_restriction)
global.forces[force_name] = global.forces[force_name] or {}
global.forces[force_name].zones_discovered = global.forces[force_name].zones_discovered or {}
global.forces[force_name].zones_discovered_count = global.forces[force_name].zones_discovered_count or 0
if system_restriction then
local star = system_restriction
while star.parent do
star = star.parent
end
local last_valid
if star.children then
for _, planet in pairs(star.children) do
if not global.forces[force_name].zones_discovered[planet.index] then
if math.random() < 0.5 then -- skip it
last_valid = planet
else
return Zone.discover(force_name, planet, source)
end
elseif planet.children then
for _, moon in pairs(planet.children) do
if not global.forces[force_name].zones_discovered[moon.index] then
if math.random() < 0.5 then -- skip it
last_valid = moon
else
return Zone.discover(force_name, moon, source)
end
end
end
end
end
end
--skipped too much
if last_valid then
return Zone.discover(force_name, last_valid, source)
end
-- find stars now
local closest_1 = nil
local closest_stellar_distance = 1000000
local pos1 = Zone.get_stellar_position(star)
for _, star2 in pairs(global.universe.stars) do
if not global.forces[force_name].zones_discovered[star2.index] then
local pos2 = Zone.get_stellar_position(star2)
local distance = util.vectors_delta_length(pos1, pos2)
if distance < closest_stellar_distance then
closest_stellar_distance = distance
closest_1 = star2
end
end
end
if closest_1 then
return Zone.discover(force_name, closest_1, source)
end
return false
end
end
function Zone.find_nearest_stellar_object(stellar_position)
local closest_distance = math.huge
local closest = nil
for _, star in pairs(global.universe.stars) do
local distance = util.vectors_delta_length(star.stellar_position, stellar_position)
if distance < closest_distance then
closest_distance = distance
closest = star
end
end
for _, space_zone in pairs(global.universe.space_zones) do
local distance = util.vectors_delta_length(space_zone.stellar_position, stellar_position)
if distance < closest_distance then
closest_distance = distance
closest = space_zone
end
end
return closest
end
function Zone.find_nearest_star(stellar_position)
local closest_distance = math.huge
local closest = nil
for _, star in pairs(global.universe.stars) do
local distance = util.vectors_delta_length(star.stellar_position, stellar_position)
if distance < closest_distance then
closest_distance = distance
closest = star
end
end
return closest
end
function Zone.find_nearest_zone(space_distortion, stellar_position, star_gravity_well, planet_gravity_well)
if space_distortion > 0.4 then return global.universe.anomaly end -- default from the anomaly
local star = Zone.find_nearest_stellar_object(stellar_position) -- can be asteroid field
if star_gravity_well > 0 then
local closest_zone = star
local closest_distance = math.abs((star.star_gravity_well or 0) - star_gravity_well)
if closest_zone.type == "star" then
for _, planet in pairs(star.children) do
local distance = math.abs(planet.star_gravity_well - star_gravity_well)
if distance < closest_distance then
closest_distance = distance
closest_zone = planet
end
end
end
if closest_zone.type == "planet" then
local closest_zone2 = closest_zone
closest_distance = math.abs(closest_zone.planet_gravity_well - planet_gravity_well)
for _, moon in pairs(closest_zone.children) do
if moon.type == "moon" then
local distance = math.abs(moon.planet_gravity_well - planet_gravity_well)
if distance < closest_distance then
closest_distance = distance
closest_zone2 = moon
end
end
end
return closest_zone2
end
return closest_zone
else
if not star then return global.universe.anomaly end
if not star.children then return star end
local last_child = star.children[#star.children]
if last_child.children then
last_child = last_child.children[#last_child.children]
end
return last_child
end
return Zone.get_default()
end
function Zone.find_nearest_solid_zone(space_distortion, stellar_position, star_gravity_well, planet_gravity_well, allow_moon)
if space_distortion == 1 then return Zone.get_default() end -- default from the anomaly
if planet_gravity_well == 0 then -- if no moon
planet_gravity_well = 100000 -- high to land on planet
end
if star_gravity_well == 0 then -- if no planet
star_gravity_well = 100000 -- high to land on planet with high solar
end
local star = Zone.find_nearest_star(stellar_position)
if not star then return Zone.get_default() end
local closest_planet = nil
local closest_distance = math.huge
for _, planet in pairs(star.children) do
if planet.type == "planet" then -- not an asteroid belt
local distance = math.abs(planet.star_gravity_well - star_gravity_well)
if distance < closest_distance then
closest_distance = distance
closest_planet = planet
end
end
end
if not closest_planet then return Zone.get_default() end
local closest_body = closest_planet -- default to planet
if allow_moon then
closest_distance = math.abs(closest_body.planet_gravity_well - planet_gravity_well)
-- see if a moon is closer
for _, moon in pairs(closest_planet.children) do
if moon.type == "moon" then
local distance = math.abs(moon.planet_gravity_well - planet_gravity_well)
if distance < closest_distance then
closest_distance = distance
closest_body = moon
end
end
end
end
return closest_body
end
function Zone.find_nearest_solid_zone_from_zone(zone)
-- typically used for escape pod
if Zone.is_solid(zone) then return nil end -- already there
if zone.type == "orbit" and Zone.is_solid(zone.parent) then
return zone.parent -- drop to planet / moon
end
return Zone.find_nearest_solid_zone(
Zone.get_space_distortion(zone),
Zone.get_stellar_position(zone),
Zone.get_star_gravity_well(zone),
Zone.get_planet_gravity_well(zone),
true
)
end
function Zone.get_force_assets(force_name, zone_index)
if not global.forces[force_name] then
if game.forces[force_name] then
setup_force(game.forces[force_name])
else
return
end
end
if not global.forces[force_name] then
if game.forces[force_name] then
game.forces[force_name].print("Error getting force data for invalid player force " .. force_name)
else
game.forces[force_name].print("Error getting force data for invalid force " .. force_name)
end
return -- invalid force
end
if not global.forces[force_name].zone_assets then
global.forces[force_name].zone_assets = {}
end
if not global.forces[force_name].zone_assets[zone_index] then
global.forces[force_name].zone_assets[zone_index] = {
rocket_launch_pad_names = {},
rocket_landing_pad_names = {},
}
end
--Log.debug_log("Zone.get_force_assets: " .. util.table_to_string(global.forces[force_name].zone_assets[zone_index]))
return global.forces[force_name].zone_assets[zone_index]
end
function Zone.get_travel_delta_v_sub(origin, destination)
-- expected ranges:
-- 1500 planetary system
-- 15000 solar system
-- 50000 interstellarsystem
-- 50000 to/from anomaly
if origin == destination then return 0 end
local origin_space_distorion = Zone.get_space_distortion(origin)
local origin_stellar_position = Zone.get_stellar_position(origin)
local origin_star_gravity_well = Zone.get_star_gravity_well(origin)
local origin_planet_gravity_well = Zone.get_planet_gravity_well(origin)
local destination_space_distorion = Zone.get_space_distortion(destination)
local destination_stellar_position = Zone.get_stellar_position(destination)
local destination_star_gravity_well = Zone.get_star_gravity_well(destination)
local destination_planet_gravity_well = Zone.get_planet_gravity_well(destination)
if origin_space_distorion > 0 then
return Zone.travel_cost_space_distortion
+ Zone.travel_cost_star_gravity * destination_star_gravity_well
+ Zone.travel_cost_planet_gravity * destination_planet_gravity_well
elseif destination_space_distorion > 0 then
return Zone.travel_cost_space_distortion
+ Zone.travel_cost_star_gravity * origin_star_gravity_well
+ Zone.travel_cost_planet_gravity * origin_planet_gravity_well
end
if origin_stellar_position.x == destination_stellar_position.x and origin_stellar_position.y == destination_stellar_position.y then
-- same solar system
if origin_star_gravity_well == destination_star_gravity_well then
-- same planetary system
return Zone.travel_cost_planet_gravity * math.abs(origin_planet_gravity_well - destination_planet_gravity_well) -- the planet_gravity_well difference
else
-- different planetary systems
return Zone.travel_cost_star_gravity * math.abs(destination_star_gravity_well - origin_star_gravity_well) -- the star_gravity_well difference
+ Zone.travel_cost_planet_gravity * origin_planet_gravity_well
+ Zone.travel_cost_planet_gravity * destination_planet_gravity_well
end
else
-- interstellar
return Zone.travel_cost_interstellar * util.vectors_delta_length(origin_stellar_position, destination_stellar_position)
+ Zone.travel_cost_star_gravity * origin_star_gravity_well
+ Zone.travel_cost_planet_gravity * origin_planet_gravity_well
+ Zone.travel_cost_star_gravity * destination_star_gravity_well
+ Zone.travel_cost_planet_gravity * destination_planet_gravity_well
end
end
function Zone.get_travel_delta_v(origin, destination)
if origin and destination then
if origin.type == "spaceship" or destination.type == "spaceship" then
return Zone.get_travel_delta_v_sub(origin, destination)
end
global.cache_travel_delta_v = global.cache_travel_delta_v or {}
global.cache_travel_delta_v[origin.index] = global.cache_travel_delta_v[origin.index] or {}
if not global.cache_travel_delta_v[origin.index][destination.index] then
global.cache_travel_delta_v[origin.index][destination.index] = Zone.get_travel_delta_v_sub(origin, destination)
end
return global.cache_travel_delta_v[origin.index][destination.index]
end
end
function Zone.get_launch_delta_v(zone)
-- 10000 to 800 for planets, 0 for in space
return 500 + (zone.radius or 50)
end
function Zone.get_closest_zone(origin, destination_list)
if origin and destination_list then
if Util.table_contains(destination_list, origin) then return origin end
local destination = nil
local delta_v_closest = math.huge
local delta_v_current = nil
for _, zone in pairs(destination_list) do
delta_v_current = Zone.get_travel_delta_v(origin, zone)
if delta_v_current < delta_v_closest then
delta_v_closest = delta_v_current
destination = zone
end
end
return destination
end
end
function Zone.find_zone_landing_position(zone, try_position)
Log.debug_log("Zone.find_zone_landing_position: " ..zone.name)
local surface = Zone.get_make_surface(zone)
if not try_position then
if zone.type == "spaceship" then
try_position = Spaceship.get_boarding_position(zone)
elseif Zone.is_solid(zone) then
local try_angle = math.random() * math.pi * 2 -- rad
local try_distance = math.random() * (zone.radius / 4 or 512)
try_position = {x = math.cos(try_angle) * try_distance, y = math.sin(try_angle) * try_distance}
else
try_position = {x = math.random(-512, 512), y = math.random(-128, 128)}
end
end
surface.request_to_generate_chunks(try_position, 2)
surface.force_generate_chunk_requests()
local safe_position
if Zone.is_solid(zone) then
safe_position = surface.find_non_colliding_position(collision_rocket_destination_surface, try_position, 64, 1)
else
safe_position = surface.find_non_colliding_position(collision_rocket_destination_orbit, try_position, 64, 1)
end
if not safe_position then
local try_position_2 = {x = 64 * (math.random() - 0.5), y = 64 * (math.random() - 0.5)}
surface.request_to_generate_chunks(try_position_2, 2)
surface.force_generate_chunk_requests()
if Zone.is_solid(zone) then
safe_position = surface.find_non_colliding_position(collision_rocket_destination_surface, try_position_2, 64, 1)
else
safe_position = surface.find_non_colliding_position(collision_rocket_destination_orbit, try_position_2, 64, 1)
end
end
if safe_position then
Log.debug_log("Zone.find_zone_landing_position: safe_position found")
return safe_position
else
Log.debug_log("Zone.find_zone_landing_position: safe_position not found, falling back to try_position")
return try_position
end
end
function Zone.dropdown_name_from_zone(zone, no_indent)
local i1 = " "
local i2 = " "
local i3 = " "
if no_indent then
i1 = ""
i2 = ""
i3 = ""
end
if zone.type == "orbit" then
if zone.parent.type == "star" then
return "[img=virtual-signal/se-star] " .. zone.name -- star orbit
elseif zone.parent.type == "planet" then
return i2.."[img=virtual-signal/se-planet-orbit] " .. zone.name -- planet orbit
elseif zone.parent.type == "moon" then
return i3.."[img=virtual-signal/se-moon-orbit] " .. zone.name -- moon orbit
end
elseif zone.type == "asteroid-belt" then
return i1 .. "[img=virtual-signal/se-asteroid-belt] " .. zone.name
elseif zone.type == "planet" then
return i1 .. "[img=virtual-signal/se-planet] " .. zone.name
elseif zone.type == "moon" then
return i2 .. "[img=virtual-signal/se-moon] " .. zone.name
elseif zone.type == "asteroid-field" then
return "[img=virtual-signal/"..mod_prefix..zone.type .. "] " .. zone.name .. " [color=black](Asteroid Field)[/color]"
elseif zone.type == "anomaly" then
return "[img=virtual-signal/"..mod_prefix..zone.type .. "] " .. zone.name .. " [color=black](Anomaly)[/color]"
elseif zone.type == "spaceship" then
return "[img=virtual-signal/"..mod_prefix..zone.type .. "] " .. zone.name .. " [color=black](Spaceship)[/color]"
end
return "[img=virtual-signal/"..mod_prefix..zone.type .. "] " .. zone.name
end
function Zone.get_alphabetised()
local zones_alphabetised = {}
for _, zone in pairs(global.zone_index) do
table.insert(zones_alphabetised, zone)
end
for _, spaceship in pairs(global.spaceships) do
table.insert(zones_alphabetised, spaceship)
end
table.sort(zones_alphabetised, function(a,b) return a.name < b.name end)
return zones_alphabetised
end
function Zone.is_visible_to_force(zone, force_name)
if global.debug_view_all_zones then return true end
if zone.type == "spaceship" then return zone.force_name == force_name end
if not (global.forces[force_name] and global.forces[force_name].zones_discovered) then return false end
return global.forces[force_name].zones_discovered[zone.index] or (zone.type == "orbit" and global.forces[force_name].zones_discovered[zone.parent.index]) and true or false
end
function Zone.insert_if_visible_to_force(target_table, zone, force_name)
if Zone.is_visible_to_force(zone, force_name) then table.insert(target_table, zone) end
end
function Zone.dropdown_list_zone_destinations(force_name, current, alphabetical, filter, wildcard, star_restriction)
-- wildcard = {list = "display", value={type = "any"}}
local selected_index = 1
local list = {""}
local values = {{type = "nil", index = nil}} -- zone indexes
local forcedata = global.forces[force_name]
if not forcedata then return list, selected_index, values end
if wildcard then
table.insert(list, wildcard.list)
table.insert(values, wildcard.value)
end
function conditional_add_zone_to_list(forcedata, current, zone)
if global.debug_view_all_zones
or (zone.type == "spaceship" and forcedata.force_name == zone.force_name)
or (zone.type ~= "spaceship" and (forcedata.zones_discovered[zone.index] or (zone.type == "orbit" and forcedata.zones_discovered[zone.parent.index]))) then
local allowed_system = true
if star_restriction then
allowed_system = false
if zone == star_restriction
or Zone.get_star_from_child(zone) == star_restriction then
allowed_system = true
end
end
if allowed_system then
if zone.type == "star" then
table.insert(list, Zone.dropdown_name_from_zone(zone.orbit, alphabetical or filter))
table.insert(values, {type = "zone", index = zone.orbit.index})
if current and zone.orbit.index == current.index then selected_index = #list end
elseif zone.type == "spaceship" then
table.insert(list, Zone.dropdown_name_from_zone(zone, alphabetical or filter))
table.insert(values, {type = "spaceship", index = zone.index})
if current and zone.type == current.type and zone.index == current.index then selected_index = #list end
else
table.insert(list, Zone.dropdown_name_from_zone(zone, alphabetical or filter))
table.insert(values, {type = "zone", index = zone.index})
if current and zone.type == current.type and zone.index == current.index then selected_index = #list end
end
end
end
end
if alphabetical == true or filter then
for _, zone in pairs(Zone.get_alphabetised()) do
if not(zone.type == "orbit" and zone.parent.type == "star") then
if (not filter) or string.find(string.lower(zone.name), string.lower(filter), 1, true) then
conditional_add_zone_to_list(forcedata, current, zone)
end
end
end
else
conditional_add_zone_to_list(forcedata, current, global.universe.anomaly)
for _, star in pairs(global.universe.stars) do
conditional_add_zone_to_list(forcedata, current, star)
for _, planet in pairs(star.children) do
conditional_add_zone_to_list(forcedata, current, planet)
if planet.children then
conditional_add_zone_to_list(forcedata, current, planet.orbit)
for _, moon in pairs(planet.children) do
conditional_add_zone_to_list(forcedata, current, moon)
conditional_add_zone_to_list(forcedata, current, moon.orbit)
end
end
end
end
for _, zone in pairs(global.universe.space_zones) do
conditional_add_zone_to_list(forcedata, current, zone)
end
for _, spaceship in pairs(global.spaceships) do
conditional_add_zone_to_list(forcedata, current, spaceship)
end
end
if star_restriction then
if filter then
list[1] = (#list - 1) .. " matching locations in system"
else
list[1] = (#list - 1) .. " known locations in system"
end
else
if filter then
list[1] = (#list - 1) .. " matching locations"
else
list[1] = (#list - 1) .. " known locations"
end
end
return list, selected_index, values
end
function Zone.build_tile_replacements(zone)
-- replaces biome_collections in replace specifications with the full biome names
--[[
convert :
biome_replacement = {
{replace={"all-dirt", "all-sand", "all-volcanic"}, with="sand-red"},
{replace={"all-vegetation", "all-frozen"}, with="vegetation-red"}
}
to
{
["tile-from-1"] = "tile-to-1",
["tile-from-2"] = "tile-to-1",
}
]]--
if zone.biome_replacements then
-- expand replacement collections
local biome_replacements_expanded = {}
for _, replacement in pairs(zone.biome_replacements) do
local replace_biomes = {}
for _, replace in pairs(replacement.replace) do
if Zone.biome_collections[replace] then -- this is a collection name
for _, biome_name in pairs(Zone.biome_collections[replace]) do
if biome_name ~= replacement.with then -- don't replace to iteself
table.insert(replace_biomes, biome_name)
end
end
elseif replace ~= replacement.with then -- this is a biome name
table.insert(replace_biomes, replace)
end
end
table.insert(biome_replacements_expanded, {replace = replace_biomes, with = replacement.with})
end
-- biome_replacements_expanded now has all of the replace names being all biome names
-- build tile map
local tile_replacements = {}
for _, replacement in pairs(biome_replacements_expanded) do
local to_tiles = Zone.biome_tiles[replacement.with]
local i = 0
for _, replace in pairs(replacement.replace) do
for _, replace_tile in pairs(Zone.biome_tiles[replace]) do
tile_replacements[replace_tile] = to_tiles[(i % #to_tiles) + 1]
i = i + 1
end
end
end
zone.tile_replacements = tile_replacements
end
end
function Zone.on_chunk_generated(event)
local area = event.area
local surface = event.surface
local zone = Zone.from_surface(surface)
if zone and Zone.is_solid(zone) then
if zone.biome_replacements then
if not zone.tile_replacements then
Zone.build_tile_replacements(zone)
end
local set_tiles = {} -- by tile name, array of positions, for the surface.set_tiles function
for x = area.left_top.x, area.right_bottom.x do
for y = area.left_top.y, area.right_bottom.y do
local tile = surface.get_tile(x, y)
if zone.tile_replacements[tile.name] then
table.insert(set_tiles, {
name = zone.tile_replacements[tile.name],
position = tile.position
})
end
end
end
if #set_tiles > 0 then
surface.set_tiles(set_tiles, true)
surface.destroy_decoratives{area = area}
surface.regenerate_decorative(nil, {{x = math.floor((area.left_top.x+16)/32), y = math.floor((area.left_top.y+16)/32)}})
end
end
end
end
Event.addListener(defines.events.on_chunk_generated, Zone.on_chunk_generated)
function Zone.export_zone(zone)
if not zone then return end
-- not safe to deepcopy
-- make all object table references id references instead.
local export_zone = Util.shallow_copy(zone)
if export_zone.orbit then
export_zone.orbit_index = export_zone.orbit.index
export_zone.orbit = nil
end
if export_zone.parent then
export_zone.parent_index = export_zone.parent.index
export_zone.parent = nil
end
if export_zone.children then
export_zone.child_indexes = {}
for i, child in pairs(export_zone.children) do
export_zone.child_indexes[i] = child.index
end
export_zone.children = nil
end
-- should be safe to deepcopy now
export_zone = Util.deep_copy(export_zone)
return export_zone
end
function Zone.get_threat(zone)
if Zone.is_solid(zone) then
if zone.is_homeworld and zone.surface_index then
local surface = Zone.get_surface(zone)
local mapgen = surface.map_gen_settings
if mapgen.autoplace_controls["enemy-base"] and mapgen.autoplace_controls["enemy-base"].size then
return math.max(0, math.min(1, mapgen.autoplace_controls["enemy-base"].size / 3)) -- 0-1
end
end
if zone.controls and zone.controls["enemy-base"] and zone.controls["enemy-base"].size then
return math.max(0, math.min(1, zone.controls["enemy-base"].size / 3)) -- 0-1
end
end
return 0
end
function Zone.get_priority(zone, force_name)
if global.forces[force_name] then
if zone.type ~= "spaceship" and global.forces[force_name].zone_priorities and global.forces[force_name].zone_priorities[zone.index] then
return global.forces[force_name].zone_priorities[zone.index]
end
if zone.type == "spaceship" and global.forces[force_name].spaceship_priorities and global.forces[force_name].spaceship_priorities[zone.index] then
return global.forces[force_name].spaceship_priorities[zone.index]
end
end
return 0
end
function Zone.get_attrition(zone, default_rate)
if default_rate == nil then default_rate = settings.global["robot-attrition-factor"].value end
if zone then
if zone.type == "spaceship" then
return 0 -- no attrition
elseif zone.type == "anomaly" then -- anomalies are dangerous, give them an absolute increase
return 10 + default_rate * 2
elseif Zone.is_solid(zone) then -- planet or moon
if zone.name == "Nauvis" or zone.is_homeworld then
return default_rate
end
local enemy = 0
if zone.controls and zone.controls["enemy-base"] and zone.controls["enemy-base"].size then
enemy = Zone.get_threat(zone) -- 0-1
end
local rate = 0.5 * (1 - enemy * 0.9)
rate = rate + 0.5 * zone.radius / 10000
if enemy == 0 then -- add a penalty to enemy free zones
rate = rate + 10
end
return rate * (0.5 + 0.5 * default_rate)
else -- space
local star_gravity_well = Zone.get_star_gravity_well(zone)
local planet_gravity_well = Zone.get_planet_gravity_well(zone)
local base_rate = star_gravity_well / 20 + planet_gravity_well / 200
local rate = 10 * (0.01 + 0.99 * base_rate) -- 0-1
return rate * (0.5 + 0.5 * default_rate)
end
end
end
function Zone.rebuild_surface_index()
global.zones_by_surface = {}
for _, zone in pairs(global.zone_index) do
if zone.surface_index then
if game.surfaces[zone.surface_index] then
global.zones_by_surface[zone.surface_index] = zone
else
zone.surface_index = nil
end
end
end
end
function Zone.trim_surface(zone, player_index)
local surface = Zone.get_surface(zone)
if not surface then return end
local protected_forces = {
friendle = "friendly",
capture = "capture",
conquest = "conquest"
}
for force_name, forcedata in pairs(global.forces) do
if game.forces[force_name] and not is_system_force(force_name) then
protected_forces[force_name] = force_name
end
end
local protected_entities = table.deepcopy(Ancient.vault_entrance_structures)
table.insert(protected_entities, Ancient.name_gate_blocker)
table.insert(protected_entities, Ancient.name_gate_blocker_void)
for name, stuff in pairs(Ancient.gate_fragments) do
table.insert(protected_entities, name)
end
local min_x = 1000000
local max_x = -1000000
local min_y = 1000000
local max_y = -1000000
local entities = surface.find_entities_filtered{name = protected_entities}
if #entities > 0 then
for _, entity in pairs(entities) do
--local p = entity.position
local b = entity.bounding_box
min_x = min_x and math.min(min_x, b.left_top.x) or b.left_top.x
min_y = min_y and math.min(min_y, b.left_top.y) or b.left_top.y
max_x = max_x and math.max(max_x, b.right_bottom.x) or b.right_bottom.x
max_y = max_y and math.max(max_y, b.right_bottom.y) or b.right_bottom.y
end
end
for chunk in surface.get_chunks() do
if surface.count_entities_filtered{force = protected_forces, area = chunk.area} > 0 then
min_x = min_x and math.min(min_x, chunk.area.left_top.x) or chunk.area.left_top.x
min_y = min_y and math.min(min_y, chunk.area.left_top.y) or chunk.area.left_top.y
max_x = max_x and math.max(max_x, chunk.area.right_bottom.x) or chunk.area.right_bottom.x
max_y = max_y and math.max(max_y, chunk.area.right_bottom.y) or chunk.area.right_bottom.y
end
end
local chunk_min_x = math.floor(min_x/32) -1
local chunk_max_x = math.floor(max_x/32) +1
local chunk_min_y = math.ceil(min_y/32) -1
local chunk_max_y = math.ceil(max_y/32) +1
local chunks_deleted = 0
for chunk in surface.get_chunks() do
if chunk.x < chunk_min_x
or chunk.x > chunk_max_x
or chunk.y < chunk_min_y
or chunk.y > chunk_max_y then
chunks_deleted = chunks_deleted + 1
surface.delete_chunk(chunk)
end
end
rendering.draw_rectangle{
color = {0,0,1},
widht = 1,
filled = false,
left_top = {min_x, min_y},
right_bottom = {max_x, max_y},
surface = surface,
time_to_live = 60
}
rendering.draw_rectangle{
color = {1,0,0},
widht = 1,
filled = false,
left_top = {chunk_min_x*32, chunk_min_y*32},
right_bottom = {chunk_max_x*32, chunk_max_y*32},
surface = surface,
time_to_live = 60
}
game.print({"space-exploration.trim-zone-results", chunks_deleted, zone.name, chunk_min_x, chunk_max_x, chunk_min_y, chunk_max_y})
--game.print("Deleted "..chunks_deleted.." from "..zone.name ..". Trimmed X ["..chunk_min_x.." to "..chunk_max_x.."] Y ["..chunk_min_y.." to "..chunk_max_y.."]")
end
function Zone.delete_surface(zone, player_index)
local player
if player_index then
player = game.players[player_index]
end
if zone.surface_index == 1 then
if player then player.print("Game cannot delete surface 1.") end
return
end
if zone.is_homeworld then
if player then player.print("Cannot delete homeworlds.") end
return
end
if zone.type == "anomaly" then
if player then player.print("Cannot delete anomalies.") end
return
end
if zone.type == "spaceship" then
if player then player.print("Cannot delete spaceships. They must be landed and dismantled.") end
return
end
if not zone.surface_index then
if player then player.print("Zone has no surface.") end
return
end
local surface = Zone.get_surface(zone)
if not surface then
--error(Zone.name.." has a surface but it was missing.")
zone.surface_index = nil
Zone.rebuild_surface_index()
return
end
local force_names = {}
for force_name, force in pairs(global.forces) do
if force.has_players and game.forces[force_name] then
table.insert(force_names, force_name)
end
end
local count_entities = surface.count_entities_filtered{force=force_names, type={"character", "electric-pole", "container", "logistic-container"}}
if count_entities > 0 then
if player then player.print("Cannot delete a zone with player entities.") end
else
log("Deleting surface for zone: " .. zone.name)
-- find all players viewing that surface
for _, player in pairs(game.connected_players) do
if player.surface == surface then
local force = player.force
local home_zone
if global.forces[force.name] and global.forces[force.name].homeworld_index then
home_zone = Zone.from_zone_index(global.forces[force.name].homeworld_index)
end
if not home_zone then home_zone = Zone.from_name("Nauvis") end
player.teleport({0,0}, Zone.get_make_surface(home_zone))
end
end
game.delete_surface(surface)
zone.surface_index = nil
zone.deleted_surface = {tick = game.tick, player_index = player_index}
Zone.rebuild_surface_index()
end
end
function Zone.on_surface_created(surface_index)
local zone = Zone.from_surface_index(surface_index)
if not zone then
game.surfaces[surface_index].solar_power_multiplier = Zone.solar_multiplier * 0.5
end
end
Event.addListener(defines.events.on_surface_created, on_surface_created)
function Zone.on_research_finished(event)
local force = event.research.force
if event.research.name == Zone.name_tech_discover_random then
local dicovered_something = Zone.discover_next_research(force.name, "Telescope data analysis", false)
if not dicovered_something then
force.print({"space-exploration.tech-discovered-nothing"})
end
elseif event.research.name == Zone.name_tech_discover_targeted then
local dicovered_something = Zone.discover_next_research(force.name, "Telescope data analysis", true)
if not dicovered_something then
force.print({"space-exploration.tech-discovered-nothing"})
end
elseif event.research.name == Zone.name_tech_discover_deep then
local dicovered_something = Zone.discover_next_research_deep_space(force.name, "Telescope data analysis", false)
if not dicovered_something then
force.print({"space-exploration.tech-deep-discovered-nothing"})
end
end
end
Event.addListener(defines.events.on_research_finished, Zone.on_research_finished)
function Zone.zone_fix_all_tiles(zone) -- or spaceship
--/c p = game.player po = p.position for x = -5,5 do for y = -5,5 do p.surface.set_tiles{{name = "dirt-1", position = {x+po.x, y=y+po.y}}} end end
local surface = Zone.get_surface(zone)
if surface then
if Zone.is_space(zone) then
if zone.type ~= "spaceship" or zone.own_surface_index then
-- remove all tiles not allowd in space
local tiles = surface.find_tiles_filtered{
collision_mask = {"ground-tile", "water-tile"},
}
local set_tiles = {}
for _, tile in pairs(tiles) do
if not Util.table_contains(tiles_allowed_in_space, tile.name) then
-- remove the tile
table.insert(set_tiles, {name = name_space_tile, position = tile.position})
surface.set_hidden_tile(tile.position, nil)
end
end
if #set_tiles > 0 then
surface.set_tiles(
set_tiles,
true, -- corect tiles
true, -- remove_colliding_entities
true, -- remove_colliding_decoratives
true -- raise_event
)
end
log("Zone.zone_fix_all_tiles: " .. zone.type.." " .. zone.name.." is_space "..#set_tiles.." tiles changed surface_index "..surface.index.." surface_name " .. surface.name)
else
log("Zone.zone_fix_all_tiles: " .. zone.type.." " .. zone.name.." anchored surface_index "..surface.index.." surface_name " .. surface.name)
end
else
-- remove space tiles
local tiles = surface.find_tiles_filtered{
collision_mask = global.named_collision_masks.space_collision_layer
}
local set_tiles = {}
for _, tile in pairs(tiles) do
if not Util.table_contains(Spaceship.names_spaceship_floors, tile.name) then -- spacehip floor is allowed
-- remove the tile
table.insert(set_tiles, {name = "nuclear-ground", position = tile.position})
surface.set_hidden_tile(tile.position, nil)
end
end
if #set_tiles > 0 then
surface.set_tiles(
set_tiles,
true, -- corect tiles
true, -- remove_colliding_entities
true, -- remove_colliding_decoratives
true -- raise_event
)
end
log("Zone.zone_fix_all_tiles: " .. zone.type.." " ..zone.name.." is_land "..#set_tiles.." tiles changed surface_index "..surface.index.." surface_name " .. surface.name)
end
end
end
function Zone.zones_fix_all_tiles()
for _, zone in pairs(global.zone_index) do
Zone.zone_fix_all_tiles(zone)
end
if global.spaceships then
for _, spaceship in pairs(global.spaceships) do
Zone.zone_fix_all_tiles(spaceship)
end
end
end
function Zone.set_zone_as_homeworld(data)
local zone = Zone.from_name(data.zone_name)
local nauvis = Zone.from_name("Nauvis")
if not zone then
game.print("No zone found")
else
if zone.type ~= "planet" then
game.print("Zone type must be planet, selected zone is: " .. zone.type)
return
else
zone.is_homeworld = true
zone.inflated = true
zone.resources = {}
zone.ticks_per_day = 25000
zone.fragment_name = "se-core-fragment-omni"
zone.radius = nauvis.radius
local nauvis_map_gen = table.deepcopy(game.surfaces[1].map_gen_settings)
if not zone.original_seed then
zone.original_seed = zone.seed
end
if data.match_nauvis_seed then
zone.seed = nauvis_map_gen.seed
else
nauvis_map_gen.seed = zone.seed
end
local surface = Zone.get_surface(zone)
if surface and reset_surface ~= false then
surface.map_gen_settings = nauvis_map_gen
surface.clear()
else
surface = Zone.get_make_surface(zone)
surface.map_gen_settings = nauvis_map_gen
end
Universe.make_validate_homesystem(zone)
global.resources_and_controls_compare_string = nil -- force udpate resources
Universe.load_resource_data()
surface.request_to_generate_chunks({0,0}, 4)
surface.force_generate_chunk_requests()
if settings.startup[mod_prefix.."spawn-small-resources"].value then
Zone.spawn_small_resources(surface)
end
end
end
return Zone.export_zone(Zone.from_name(data.zone_name))
end
function Zone.spawn_small_resources(surface)
local seed = surface.map_gen_settings.seed
local rng = game.create_random_generator(seed)
-- The starting resourecs of the map generation are inconsistent and spread out.
-- Add some tiny patches to reduce the amount of running around at the start.
-- We only care about super-early game, so just iron, copper, stone, and coal.
-- If there are other resources added to the game then the naturally spawned resources will have to do for now.
-- These resources are not designed to replace the normal starting resources at all.
local valid_position_search_range = 256
local cluster_primary_radius = 50 -- get away from crash site
local cluster_secondary_radius = 25
local resources = {}
if game.entity_prototypes["iron-ore"] then table.insert(resources, { name = "iron-ore", tiles = 200, amount = 100000}) end
if game.entity_prototypes["copper-ore"] then table.insert(resources, { name = "copper-ore", tiles = 150, amount = 80000}) end
if game.entity_prototypes["stone"] then table.insert(resources, { name = "stone", tiles = 150, amount = 80000}) end
if game.entity_prototypes["coal"] then table.insert(resources, { name = "coal", tiles = 150, amount = 80000}) end
local cluster_orientation = rng()
local secondary_orientation = rng()
local cluster_position = Util.orientation_to_vector(cluster_orientation, cluster_primary_radius)
surface.request_to_generate_chunks(cluster_position, 4)
surface.force_generate_chunk_requests()
Log.trace("[gps="..math.floor(cluster_position.x)..","..math.floor(cluster_position.y).."]")
local closed_tiles = {} -- 2d disctionary
local open_tiles = {} -- 1d array
local function close_tile(position)
closed_tiles[position.x] = closed_tiles[position.x] or {}
closed_tiles[position.x][position.y] = true
end
local function open_tile(set, position) -- don't open if closed
if not (closed_tiles[position.x] and closed_tiles[position.x][position.y]) then
table.insert(set, position)
close_tile(position)
end
end
local function open_neighbour_tiles(set, position)
open_tile(set, Util.vectors_add(position, {x=0,y=-1}))
open_tile(set, Util.vectors_add(position, {x=1,y=0}))
open_tile(set, Util.vectors_add(position, {x=0,y=1}))
open_tile(set, Util.vectors_add(position, {x=-1,y=0}))
end
for i, resource in pairs(resources) do
resource.orientation = secondary_orientation + rng()
local offset = Util.orientation_to_vector(resource.orientation, rng(cluster_secondary_radius/2, cluster_secondary_radius))
local position = Util.tile_to_position(Util.vectors_add(offset, cluster_position))
local valid = surface.find_non_colliding_position(resource.name, position, valid_position_search_range, 1, true)
if not valid then Log.trace("no valid position found") end
resource.start_point = surface.find_non_colliding_position(resource.name, position, valid_position_search_range, 1, true) or position
resource.open_tiles = {resource.start_point}
resource.entities = {}
resource.amount_placed = 0
end
local continue = true
local repeats = 0
while continue and repeats < 1000 do
repeats = repeats + 1
continue = false
for _, resource in pairs(resources) do
--if #resource.entities < resource.tiles then
if resource.amount_placed < resource.amount then
continue = true
local try_tile
if #resource.open_tiles > 0 then
local choose = rng(#resource.open_tiles)
try_tile = resource.open_tiles[choose]
close_tile(try_tile)
end
if not try_tile then -- handle tiny island case
try_tile = resource.start_point
end
local position = surface.find_non_colliding_position(resource.name, try_tile, valid_position_search_range, 1, true)
if not position then -- exit
resource.amount_placed = resource.amount
log("Space Exploration failed to place starting resource, no valid positions in range. [".. resource.name.."]")
else
close_tile(try_tile)
close_tile(position)
local remaining = resource.amount - resource.amount_placed
local amount = math.ceil(math.min( remaining * (0.01 + rng() * 0.005) + 100 + rng() * 100, remaining))
resource.amount_placed = resource.amount_placed + amount
table.insert(resource.entities, surface.create_entity{name = resource.name, position=position, amount=amount, enable_tree_removal=true, snap_to_tile_center =true})
--Log.trace("Starting resource entity created "..resource.name.." ".. position.x.." "..position.y)
open_neighbour_tiles(resource.open_tiles, position)
end
end
end
end
end
return Zone