/
resource-autoplace.lua
359 lines (321 loc) · 15.7 KB
/
resource-autoplace.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
local noise = require("noise")
local expression_to_ascii_math = require("noise.expression-to-ascii-math")
local tne = noise.to_noise_expression
local litexp = noise.literal_expression
starting_resource_count = 5
regular_resource_count = 10 -- add oil and uranium
region_size = 1024
starting_size = 120
next_resource_index = 0
function get_next_resource_index()
local resource_index = next_resource_index
next_resource_index = next_resource_index + 1
return resource_index
end
local pointillist_mode = false
local patch_blobbiness_enabled = true
local function dump_expression(name, expr)
log(name..":\n"..tostring(expression_to_ascii_math(expr)))
end
local onethird = tne(1)/3 -- Looks nicer in output than 0.333333
local function resource_autoplace_settings(params)
local name = params.name
local order = params.order or "d"
-- How much of this stuff (probability * richness) should occur per tile on average near the starting area?
local base_density = params.base_density
-- Random probability that this stuff is placed when probability is otherwise positive
-- This IS automatically compensated for by richness, so you don't need to adjust base_density
local random_probability = params.random_probability or 1
local base_spots_per_km2 = params.base_spots_per_km2 or 2.5
local random_spot_size_minimum = params.random_spot_size_minimum or 0.25
local random_spot_size_maximum = params.random_spot_size_maximum or 2.00
-- Amplitude of spot 'blob noise' relative to typical spot amplitude
local regular_blob_amplitude_multiplier = 1/8 * (params.regular_blob_amplitude_multiplier or 1)
local starting_blob_amplitude_multiplier = 1/8 * (params.starting_blob_amplitude_multiplier or 1)
local control_setting = noise.get_control_setting(name)
local frequency_multiplier = control_setting.frequency_multiplier
local size_multiplier = control_setting.size_multiplier
local density_multiplier = frequency_multiplier * size_multiplier
-- The following are more dangerous, since they'll throw total quantities off if you don't compensate for them:
-- additional_richness will be added to richness but does not affect probability of anything being placed at all.
-- This is NOT automatically compensated for, because that would be difficult to calculate.
-- The caller will need to compensate for any additional_richness by adjusting base_density.
local additional_richness = params.additional_richness or 0
-- richness will be clamped to minimum_richness at the low end anywhere the stuff is otherwise placed
-- Not automatically compensated for.
local minimum_richness = params.minimum_richness or 0
-- 'post' as in multiplied after everything else is calculated, including additional_richness
-- and minimum_richness.
local richness_post_multiplier = (params.richness_post_multiplier or 1) * control_setting.richness_multiplier
local seed1 = 100
-- rq_factor is the ratio of the radius of a patch to the cube root of its quantity,
-- i.e. radius of a quantity=1 patch; higher values = fatter, shallower patches
-- Watch out! Shallower patches are more heavily thrown off by noise,
-- so adjust noise amplitude accordingly!
-- (this is automatically done -- se *_blob_amplitude, below)
local regular_rq_factor = (params.regular_rq_factor_multiplier or 1) * 1 / 10
local starting_rq_factor = (params.starting_rq_factor_multiplier or 1) * 1 / 7
local elevation = noise.var("elevation")
local distance = noise.var("distance")
-- > I just realized because of the new rule of "keep ores outside the
-- > starting area", the starting area size setting should not affect ore
-- > placement. The ore's starting area should be it's own fixed value
-- > that ignores the setting.
-- Twinsen, August 2018
starting_resource_placement_radius = starting_size
local regular_modulation
-- has_starting_area_placement values:
-- - true - place in starting area and outside starting area independently
-- - false - place outside starting area but not inside
-- - nil - place everywhere as if there is no starting area
local regular_patch_fade_in_distance = 300
local regular_ns_multiplier_at
if params.has_starting_area_placement == nil then
regular_ns_multiplier_at = function(dist) return 1 end
else
regular_ns_multiplier_at = function(dist)
return noise.clamp((dist - starting_resource_placement_radius) / regular_patch_fade_in_distance, 0, 1)
end
end
local double_density_distance = 1300 -- distance at which patches have twice as much stuff in them
-- Maximum distance at which blob amplitude should keep increasing along with spot height
local regular_blob_amplitude_maximum_distance = double_density_distance
local spot_enlargement_maximum_distance = regular_blob_amplitude_maximum_distance
-- Get distance for purposes of calculating regular ore density, patch size, and richness
local function size_effective_distance_at(dist)
if params.has_starting_area_placement == nil then
return dist
else
-- If there's a starting area measure from the edge of the fade-in radius
return dist - regular_patch_fade_in_distance
end
end
local function regular_density_at(dist)
-- Don't increase density beyond spot_enlargement_maximum_distance
-- because large spots get unwieldy. We'll increase richness after that, instead.
effective_distance = noise.clamp(size_effective_distance_at(dist), 0, spot_enlargement_maximum_distance)
local distance_density_multiplier = 1 + effective_distance / double_density_distance
return base_density * density_multiplier * distance_density_multiplier * regular_ns_multiplier_at(dist)
end
local spots_per_km2_near_start = base_spots_per_km2 * frequency_multiplier
local candidate_point_count = tne(128)
if pointillist_mode then
-- Split ore into lots and lots and lots of little patches
-- so that we can get a better idea of the underlying distribution
candidate_point_count = 10000 -- hardcoded max so we don't melt the player's CPU
spots_per_km2_near_start = candidate_point_count / regular_resource_count
end
-- Regular spot quantity without randomization added
local function regular_spot_quantity_base_at(dist)
return regular_density_at(dist) * 1000000 / spots_per_km2_near_start
end
-- Regular spot quantity averaging over randomization
local function regular_spot_quantity_typical_at(dist)
local average_random_size_multiplier = (random_spot_size_minimum + random_spot_size_maximum) / 2
return average_random_size_multiplier * regular_spot_quantity_base_at(dist)
end
local function regular_spot_height_typical_at(dist)
return regular_spot_quantity_typical_at(dist)^(onethird) / ((math.pi/3) * regular_rq_factor^2)
end
local regular_density_expression = regular_density_at(distance)
local regular_spot_quantity_expression = noise.random_between(random_spot_size_minimum, random_spot_size_maximum) * regular_spot_quantity_base_at(distance)
local regular_spot_radius_expression = noise.min(32, regular_rq_factor * regular_spot_quantity_expression ^ (onethird))
if params.has_starting_area_placement ~= nil then
regular_blob_amplitude_maximum_distance = regular_blob_amplitude_maximum_distance + regular_patch_fade_in_distance
end
local function regular_blob_amplitude_at(dist)
return regular_blob_amplitude_multiplier * noise.min(
regular_spot_height_typical_at(regular_blob_amplitude_maximum_distance),
regular_spot_height_typical_at(dist)
)
end
local regular_blob_amplitude_maximum = regular_blob_amplitude_at(regular_blob_amplitude_maximum_distance)
local regular_blob_amplitude_expression = regular_blob_amplitude_at(distance)
-- Values for starting spots.
-- Simpler calculations than for regular spots because they are only placed
-- in one place and therefore there are fewer variables!
local starting_amount = 40000 * base_density * density_multiplier
--local starting_amount = 1000000 -- nicer for testing - just check that all spots have ~1.0M
local starting_area_sharpness = tne(math.huge)
local starting_resource_placement_area = math.pi*starting_resource_placement_radius*starting_resource_placement_radius
local starting_density = starting_amount / starting_resource_placement_area
-- Goes < 0 outside of starting area and at negative elevations
local starting_modulation =
noise.clamp((starting_resource_placement_radius - distance) * starting_area_sharpness, 0, 1)
local starting_feasibility =
noise.clamp((elevation - 1) / 10, 0, 1) * starting_modulation
-- Allow resources at lower elevations for starting
-- Set minimum_favorability_for_full_placement to lower numbers to decrease the likelihood that the starting patches get split.
-- Quantity will automatically be clamped by the spot noise function
-- and radius will be automatically adjusted, too,
-- so it's fine for the spot quantity to be more than the region target quantity.
local minimum_favorability_for_full_placement = 1/2
local starting_spot_count = frequency_multiplier
local starting_area_spot_quantity = starting_amount / minimum_favorability_for_full_placement / starting_spot_count
local starting_spot_height = starting_area_spot_quantity ^ (1/3) / ((math.pi/3) * starting_rq_factor^2)
local starting_blob_amplitude = starting_blob_amplitude_multiplier * starting_spot_height
-- since starting and regular spots get maxed together,
-- the basement value should be the lower of the two.
-- This value needs to be low enough that any noise added to it is still below zero
-- so that we don't get bits of ores sticking out between spot noise spots.
-- It also needs to be constant because that's how the spot noise op works.
-- Simply using -infinity would work, but calculating it based on blob amplitude:
-- a) looks nicer if you render the value on a map preview
-- b) acts as a check on our blob_amplitude calculations
local basement_value = noise.min(-6 * regular_blob_amplitude_maximum,
-6 * starting_blob_amplitude)
local regular_spots = tne{
type = "function-application",
function_name = "spot-noise",
arguments =
{
x = noise.var("x"),
y = noise.var("y"),
seed0 = noise.var("map_seed"),
seed1 = tne(seed1),
region_size = tne(region_size),
candidate_point_count = tne(candidate_point_count),
skip_span = tne(regular_resource_count),
skip_offset = tne(params.resource_index or 0),
density_expression = litexp(regular_density_expression), -- low-frequency noise evaluate for an entire region
spot_quantity_expression = litexp(regular_spot_quantity_expression), -- used to figure out where spots go
hard_region_target_quantity = tne(false), -- it's fine for large spots to push region quantity past the target
spot_radius_expression = litexp(regular_spot_radius_expression),
spot_favorability_expression = litexp(1),
basement_value = basement_value,
maximum_spot_basement_radius = tne(128)
}
}
-- If you change starting area region size,
-- also change the default starting area position in MapGenSettings
local starting_spots = tne{
type = "function-application",
function_name = "spot-noise",
arguments =
{
x = noise.var("x"),
y = noise.var("y"),
seed0 = noise.var("map_seed"),
seed1 = tne(seed1+1),
skip_span = tne(starting_resource_count),
skip_offset = tne(params.resource_index or 0),
region_size = tne(starting_resource_placement_radius * 2),
candidate_point_count = tne(128),
minimum_candidate_point_spacing = tne(32),
density_expression = litexp(starting_density * starting_modulation),
spot_quantity_expression = litexp(starting_area_spot_quantity),
hard_region_target_quantity = tne(true), -- Since there's [usually] only one spot, clamp its quantity to the target quantity
spot_radius_expression = litexp(starting_rq_factor * starting_area_spot_quantity ^ (onethird)),
spot_favorability_expression = litexp(
starting_feasibility * 2 -
1 * distance / starting_resource_placement_radius +
noise.random(0.5)
),
basement_value = basement_value,
maximum_spot_basement_radius = tne(128) -- does making this huge make a difference?
}
}
if pointillist_mode or not patch_blobbiness_enabled then
regular_blob_amplitude_expression = 0
end
if not patch_blobbiness_enabled then
starting_blob_amplitude = 0
end
-- Add some blobbiness
local blobs0 = tne{
type = "function-application",
function_name = "factorio-basis-noise",
arguments =
{
x = noise.var("x"),
y = noise.var("y"),
seed0 = noise.var("map_seed"),
seed1 = tne(seed1),
input_scale = tne(1/8),
output_scale = tne(1)
}
} + tne{
type = "function-application",
function_name = "factorio-basis-noise",
arguments =
{
x = noise.var("x"),
y = noise.var("y"),
seed0 = noise.var("map_seed"),
seed1 = tne(seed1),
input_scale = tne(1/24),
output_scale = tne(1)
}
}
local blobs0f = blobs0 - 1/4
local blobs1 = blobs0 + tne{
type = "function-application",
function_name = "factorio-basis-noise",
arguments =
{
x = noise.var("x"),
y = noise.var("y"),
seed0 = noise.var("map_seed"),
seed1 = tne(seed1),
input_scale = tne(1/64),
output_scale = tne(1.5)
}
}
local blobs1f = blobs1 - onethird -- attempt to remove positive bias
local regular_patches = regular_spots + blobs1f * regular_blob_amplitude_expression
local starting_patches = starting_spots + blobs0f * starting_blob_amplitude
local all_patches
if params.has_starting_area_placement == true then
all_patches = noise.max(starting_patches, regular_patches)
elseif params.has_starting_area_placement == false then
all_patches = regular_patches
else -- nil or unspecified means just make it uniform everywhere
all_patches = regular_patches
end
local richness_expression = noise.delimit_procedure(all_patches) -- Re-use all that stuff between richness/probability!
local probability_expression = noise.clamp(richness_expression, 0, 1)
if random_probability < 1 then
richness_expression = richness_expression / random_probability
probability_expression = probability_expression * tne{
type = "function-application",
function_name = "random-penalty",
arguments =
{
source = tne(1),
x = noise.var("x"),
y = noise.var("y"),
amplitude = tne(1/random_probability) -- put random_probability points with probability < 0
}
}
end
if additional_richness > 0 then
richness_expression = richness_expression + additional_richness
end
if minimum_richness > 0 then
richness_expression = noise.max(richness_expression, minimum_richness)
end
-- sed = size-effective distance
local function post_semd_richness_distance_multiplier_at(sed)
local ddd = double_density_distance
local semd = spot_enlargement_maximum_distance
-- density = pre-richness-mutliplied density * richness_distance_multiplier.
-- Since pre-richness-multiplied density plateaus at semd,
-- richness needs to increase at that point, and by this much:
return (ddd + sed)/(ddd + semd)
end
local richness_distance_multiplier = noise.max(1, post_semd_richness_distance_multiplier_at(size_effective_distance_at(distance)))
richness_expression = richness_expression * richness_distance_multiplier * richness_post_multiplier
local ret =
{
order = order,
control = name,
probability_expression = probability_expression,
richness_expression = richness_expression
}
return ret
end
return
{
get_next_resource_index = get_next_resource_index,
resource_autoplace_settings = resource_autoplace_settings
}