File size: 19,788 Bytes
898c672
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
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

local function get_patch_metaset_patch_set_index(patch_metaset, patch_set_name)
  if patch_metaset.patch_set_indexes[patch_set_name] == nil then
    patch_metaset.patch_set_indexes[patch_set_name] = patch_metaset.next_patch_set_index
    patch_metaset.next_patch_set_index = patch_metaset.next_patch_set_index + 1
    data.raw["noise-expression"][patch_metaset.count_expression_name].expression = tne(patch_metaset.next_patch_set_index)
  end
  return patch_metaset.patch_set_indexes[patch_set_name]
end

local function new_patch_metaset(params)
  data.raw["noise-expression"][params.count_expression_name] =
  {
    type = "noise-expression",
    name = params.count_expression_name,
    expression = tne(0)
  }

  return
  {
    count_expression_name = params.count_expression_name,
    next_patch_set_index = 0,
    patch_set_indexes = {},
    get_patch_set_index = get_patch_metaset_patch_set_index
  }
end

-- This has to be global so that all calls into this library
-- use the same object and get unique patchset indexes.
-- It is not indended to be used directly from outside of this library.
if not resource_autoplace__patch_metasets then
  resource_autoplace__patch_metasets =
  {
    regular = new_patch_metaset{ count_expression_name = "regular-resource-patch-set-count" },
    starting = new_patch_metaset{ count_expression_name = "starting-resource-patch-set-count" }
  }
end

local regular_patch_metaset = resource_autoplace__patch_metasets.regular
local starting_patch_metaset = resource_autoplace__patch_metasets.starting

-- Indicate that a patch set exists and optionally that it also needs a separate starting patch set.
-- Call this to initialize patch sets' indexes in a more deterministic order
-- (see resources.lua for an example) before calling resource_autoplace_settings.
local function initialize_patch_set(patch_set_name, has_starting_area_placement)
  regular_patch_metaset:get_patch_set_index(patch_set_name)
  if has_starting_area_placement then
    starting_patch_metaset:get_patch_set_index(patch_set_name)
  end
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

--- Creates and returns an AutoplaceSpecification that will generate spot-based ore patches.
-- Required parameters:
-- - name - name for the type, used as the default autoplace control name and patch set name
--   (each of which can be overridden separately)
-- - base_density - amount of stuff, on average, to be placed per tile
-- Optional parameters:
-- - patch_set_name - name of the patch set; patches sets of the same name and seed1 will overlap; default: name
-- - autoplace_control_name - name of the corresponding autoplace control; default: name
-- - random_probability - probability of placement at any given tile within a patch; default: 1
-- - base_spots_per_km2 - number of patches per square kilometer near the starting area
-- - has_starting_area_placement - true|false|nil - yes, no, and there is no special starting area, respectively
-- - seed1 - random seed to use when generating patch positions; default: 100
-- More obscure parameters can be read about in the inline comments.
local function resource_autoplace_settings(params)
  local name = params.name
  local order = params.order or "d"

  local patch_set_name = params.patch_set_name or name
  local autoplace_control_name = params.autoplace_control_name or name

  -- 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(autoplace_control_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 = params.seed1 or 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
  local starting_resource_placement_radius = 120
  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_spot_count = params.candidate_spot_count or 21

  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_spot_count = 10000 -- hardcoded max so we don't melt the player's CPU
    spots_per_km2_near_start = candidate_spot_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!

  -- reduce the influence of the frequency slider over the amount of ore in the starting area.
  -- note that starting_spot_count is still set to frequency_multiplier below, so we still split the ore to a fairly high amount of patches.
  local starting_frequency_multiplier = ((frequency_multiplier - 1) * 0.5) + 1
  local starting_amount = 40000 * base_density * starting_frequency_multiplier * size_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(1024),
      candidate_spot_count = tne(candidate_spot_count),
      suggested_minimum_candidate_point_spacing = tne(45.254833995939045), -- Magic number to match 0.17.50 spot placement, when candidate_point_count was always 128
      skip_span = noise.var("regular-resource-patch-set-count"),
      skip_offset = tne(regular_patch_metaset:get_patch_set_index(patch_set_name)),
      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)
    }
  }

  -- Don't want to distrurb starting_patch_metaset unless we actually need to.
  local starting_patch_set_index = 0
  if params.has_starting_area_placement == true then
    starting_patch_set_index = tne(starting_patch_metaset:get_patch_set_index(patch_set_name))
  end

  -- 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 = noise.var("starting-resource-patch-set-count"),
      skip_offset = starting_patch_set_index,
      region_size = tne(starting_resource_placement_radius * 2),
      candidate_spot_count = tne(32),
      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 = autoplace_control_name,
    probability_expression = probability_expression,
    richness_expression = richness_expression
  }

  return ret
end

return
{
  initialize_patch_set = initialize_patch_set,
  resource_autoplace_settings = resource_autoplace_settings
}