diff --git a/map_gen/maps/danger_ores/compatibility/omnimatter/ores.lua b/map_gen/maps/danger_ores/compatibility/omnimatter/ores.lua index ce789aa9e..7bb5ed06e 100644 --- a/map_gen/maps/danger_ores/compatibility/omnimatter/ores.lua +++ b/map_gen/maps/danger_ores/compatibility/omnimatter/ores.lua @@ -1,34 +1,15 @@ -local b = require 'map_gen.shared.builders' -local start_value = b.euclidean_value(0, 0.35) -local value = b.exponential_value(0, 0.06, 1.55) +-- The Omnimatter configs (Omnimatter, Cages, Maze): one omnite sector wearing every +-- terrain skin, so the world keeps its natural look. +local Factory = require 'map_gen.maps.danger_ores.config.main_ores_factory' -return { - { - name = 'omnite', - ['tiles'] = { - [1] = 'red-desert-0', - [2] = 'red-desert-1', - [3] = 'red-desert-2', - [4] = 'red-desert-3', - [5] = 'dirt-1', - [6] = 'dirt-2', - [7] = 'dirt-3', - [8] = 'dirt-4', - [9] = 'dirt-5', - [10] = 'dirt-6', - [11] = 'dirt-7', - [12] = 'grass-1', - [13] = 'grass-2', - [14] = 'grass-3', - [15] = 'grass-4', - [16] = 'sand-1', - [17] = 'sand-2', - [18] = 'sand-3', - }, - ['start'] = start_value, - ['weight'] = 1, - ['ratios'] = { - {resource = b.resource(b.full_shape, 'omnite', value), weight = 100}, - } - }, -} +return Factory.blank() + :add_ore('omnite', { + mix = {{'omnite', 100}}, + tiles = { + 'red-desert-0', 'red-desert-1', 'red-desert-2', 'red-desert-3', + 'dirt-1', 'dirt-2', 'dirt-3', 'dirt-4', 'dirt-5', 'dirt-6', 'dirt-7', + 'grass-1', 'grass-2', 'grass-3', 'grass-4', + 'sand-1', 'sand-2', 'sand-3' + } + }) + :richness{mult = 0.06, pow = 1.55} diff --git a/map_gen/maps/danger_ores/config/main_ores_factory.lua b/map_gen/maps/danger_ores/config/main_ores_factory.lua index a2d7b7e01..5114b10f4 100644 --- a/map_gen/maps/danger_ores/config/main_ores_factory.lua +++ b/map_gen/maps/danger_ores/config/main_ores_factory.lua @@ -314,6 +314,49 @@ function Fluent:start_value(start, ore_name) return rebuild(self) end +-- Multiply a curve by a factor: numbers scale directly, functions get wrapped. +local function scale_curve(curve, multiplier) + if curve == nil or multiplier == 1 then + return curve + end + if type(curve) == 'number' then + return multiplier * curve + end + return function(x, y) + return multiplier * curve(x, y) + end +end + +--- Scale the ore density: multiply every start and richness curve by multiplier -- +-- for one sector, or the whole config. e.g. :scale_richness(0.25) for a quarter of +-- the source config's ore. Entries with a custom make_resource are not affected. +function Fluent:scale_richness(multiplier, ore_name) + local spec = getmetatable(self).spec + if ore_name then + for _, ore in ipairs(spec.ores) do + if ore.name == ore_name then + -- materialize the defaults before scaling only this sector + if ore.start == nil then + ore.start = spec.start + end + if ore.value == nil then + ore.value = spec.value + end + ore.start = scale_curve(ore.start, multiplier) + ore.value = scale_curve(ore.value, multiplier) + end + end + else + spec.start = scale_curve(spec.start, multiplier) + spec.value = scale_curve(spec.value, multiplier) + for _, ore in ipairs(spec.ores) do + ore.start = scale_curve(ore.start, multiplier) + ore.value = scale_curve(ore.value, multiplier) + end + end + return rebuild(self) +end + --- Set per-sector selection weights from a map of ore name -> weight. function Fluent:sector_weights(weights) local spec = getmetatable(self).spec @@ -325,4 +368,92 @@ function Fluent:sector_weights(weights) return rebuild(self) end +-- == Resource patches ======================================================= +-- A fluent builder for resource patches configs (main_ore_resource_patches_config / +-- resource_patches_config): perlin-noise patches of dense resources on top of the +-- main ores. Same contract as the main-ores fluent API: the returned object IS the +-- patches array, every method mutates it in place and returns it for chaining, and +-- method order never matters. +-- Factory.patches():add_patch('stone', {scale = 1 / 32, threshold = 0.6}) +-- Factory.patches(shared_config):scale{richness = 0.5, size = 2} + +local PatchesFluent = {} + +-- The canonical patch richness curve, as used by the vanilla resource patches. +local default_patch_value = b.exponential_value(0, 1.4, 1.45) + +-- Multiply the amounts a resource shape yields. +local function scale_amount(resource, multiplier) + if multiplier == 1 then + return resource + end + return function(x, y, world) + local entity = resource(x, y, world) + if entity and entity.amount then + entity.amount = multiplier * entity.amount + end + return entity + end +end + +local function rebuild_patches(self) + local spec = getmetatable(self).spec + for i = #self, 1, -1 do + self[i] = nil + end + for index, patch in ipairs(spec.patches) do + local resource + if patch.resource then -- entry carried over from a source config + resource = scale_amount(patch.resource, spec.richness) + else + resource = b.resource(b.full_shape, patch.name, scale_curve(patch.value or default_patch_value, spec.richness)) + end + self[index] = { + scale = patch.scale / spec.size, + threshold = patch.threshold, + seed = patch.seed, + resource = resource + } + end + return self +end + +--- A fluent resource patches config, optionally seeded from an existing plain +-- config. The source is not modified; its entries are carried over and can still +-- be scaled. +function Public.patches(source) + local patches = {} + for i, data in ipairs(source or {}) do + patches[i] = {scale = data.scale, threshold = data.threshold, seed = data.seed, resource = data.resource} + end + local spec = {patches = patches, richness = 1, size = 1} + return rebuild_patches(setmetatable({}, {__index = PatchesFluent, spec = spec})) +end + +--- Append a pure-resource patch. opts (all optional): scale (patch size, default +-- 1 / 24), threshold (rarity, default 0.5), richness (a function or +-- {base = b, mult = m, pow = p}, default the canonical patch curve) and seed. +function PatchesFluent:add_patch(resource_name, opts) + opts = opts or {} + local spec = getmetatable(self).spec + spec.patches[#spec.patches + 1] = { + name = resource_name, + scale = opts.scale or 1 / 24, + threshold = opts.threshold or 0.5, + value = normalize_value(opts.richness), + seed = opts.seed + } + return rebuild_patches(self) +end + +--- Scale every patch: richness multiplies the ore amounts, size multiplies the +-- linear patch dimensions (patch spacing grows with it, so the ore coverage +-- fraction stays the same). Multipliers compound when applied twice. +function PatchesFluent:scale(multipliers) + local spec = getmetatable(self).spec + spec.richness = spec.richness * (multipliers.richness or 1) + spec.size = spec.size * (multipliers.size or 1) + return rebuild_patches(self) +end + return Public diff --git a/map_gen/maps/danger_ores/config/ore_configs.test.lua b/map_gen/maps/danger_ores/config/ore_configs.test.lua index a3d2a0dab..6cba14420 100644 --- a/map_gen/maps/danger_ores/config/ore_configs.test.lua +++ b/map_gen/maps/danger_ores/config/ore_configs.test.lua @@ -218,6 +218,91 @@ local d1, d2 = Factory.default(), Factory.default() check(not rawequal(d1, d2) and not rawequal(d1[1], d2[1]) and not rawequal(d1[1].ratios, d2[1].ratios), 'every fluent config is built from fresh tables') +-- function curves so the scaled results stay callable under the stubbed builders +local function flat(amount) + return function() + return amount + end +end + +local function test_scale_richness() + local scaled = Factory.blank() + :add_ore('scrap', {mix = {{'scrap', 100}}}) + :richness(flat(100)) + :start_value(flat(40)) + :scale_richness(0.25) + check(scaled[1].ratios[1].resource.value() == 25 and scaled[1].start() == 10, + 'scale_richness scales the richness and start curves') + + local half_coal = Factory.default() + :richness(flat(100)) + :start_value(50) + :scale_richness(0.5, 'coal') + check(half_coal[2].ratios[1].resource.value() == 50 and half_coal[2].start == 25 + and half_coal[1].ratios[1].resource.value() == 100 and half_coal[1].start == 50, + 'scale_richness with an ore name scales only that sector') + + local compounded = Factory.blank() + :add_ore('omnite') + :richness(flat(100)) + :start_value(8) + :scale_richness(0.5) + :scale_richness(0.5) + check(compounded[1].ratios[1].resource.value() == 25 and compounded[1].start == 2, + 'scale_richness compounds when applied twice') + + local scaled_then_added = Factory.blank() + :richness(flat(100)) + :start_value(flat(40)) + :scale_richness(0.25) + :add_ore('scrap', {mix = {{'scrap', 100}}}) + check(scaled_then_added[1].ratios[1].resource.value() == 25 and scaled_then_added[1].start() == 10, + 'sectors added after scale_richness still get the scaled defaults') +end +test_scale_richness() + +local function test_patches() + local stone = Factory.patches() + :add_patch('stone', {scale = 1 / 32, threshold = 0.6, richness = flat(100)}) + check(#stone == 1 and stone[1].scale == 1 / 32 and stone[1].threshold == 0.6 + and stone[1].resource.name == 'stone' and stone[1].resource.value() == 100, + 'patches():add_patch builds a pure resource patch entry') + + stone:scale{richness = 0.25, size = 2} + check(stone[1].scale == 1 / 64 and stone[1].resource.value() == 25, + 'patches scale multiplies richness and grows the patch size') + + local canonical = Factory.patches():add_patch('iron-ore') + check(canonical[1].scale == 1 / 24 and canonical[1].threshold == 0.5 + and canonical[1].resource.value == 'exp(0,1.4,1.45)', + 'add_patch defaults to the canonical patch size, rarity and curve') + + local source = { + {scale = 1 / 24, threshold = 0.5, resource = function() + return {name = 'iron-ore', amount = 100} + end}, + {scale = 1 / 24, threshold = 0.5, resource = function() + return nil + end} + } + local wrapped = Factory.patches(source):scale{richness = 0.5, size = 2} + check(wrapped[1].scale == 1 / 48 and wrapped[1].resource(0, 0, {}).amount == 50, + 'patches(source) carries over an existing config and scales it') + check(wrapped[2].resource(0, 0, {}) == nil, + 'patches(source) preserves empty resource results') + check(source[1].scale == 1 / 24 and source[1].resource(0, 0, {}).amount == 100, + 'patches(source) does not modify the original config') + + local compounded = Factory.patches(source):scale{richness = 0.5}:scale{richness = 0.5} + check(compounded[1].resource(0, 0, {}).amount == 25, + 'patches scale compounds when applied twice') + + local late = Factory.patches():scale{richness = 0.5}:add_patch('stone', {richness = flat(100)}) + check(late[1].resource.value() == 50, + 'patches added after scale still get the scale applied') +end +test_patches() + -- 5) golden numbers for the source configs, so old maps cannot drift by accident local function ratio_weights(entry) local weights = {} diff --git a/map_gen/maps/danger_ores/presets/danger_ore_coal_maze.lua b/map_gen/maps/danger_ores/presets/danger_ore_coal_maze.lua index ac34289ca..f6993429d 100644 --- a/map_gen/maps/danger_ores/presets/danger_ore_coal_maze.lua +++ b/map_gen/maps/danger_ores/presets/danger_ore_coal_maze.lua @@ -1,5 +1,6 @@ local B = require 'map_gen.shared.builders' local DOC = require 'map_gen.maps.danger_ores.configuration' +local Factory = require 'map_gen.maps.danger_ores.config.main_ores_factory' local Scenario = require 'map_gen.maps.danger_ores.scenario' local ScenarioInfo = require 'features.gui.info' @@ -13,7 +14,9 @@ DOC.scenario_name = 'danger-ore-coal-maze' DOC.map_config.spawn_shape = B.translate(B.rectangle(64), 2, 2) DOC.map_config.start_ore_shape = B.translate(B.rectangle(68), 2, 2) DOC.map_config.no_resource_patch_shape = B.translate(B.rectangle(80), 2, 2) -DOC.map_config.main_ore_resource_patches_config = require 'map_gen.maps.danger_ores.config.main_ore_resource_patches' +DOC.map_config.main_ore_resource_patches_config = Factory + .patches(require 'map_gen.maps.danger_ores.config.main_ore_resource_patches') + :scale{richness = 0.5, size = 2} DOC.map_config.main_ores_builder = require 'map_gen.maps.danger_ores.modules.main_ores_patches' DOC.map_config.main_ores = require 'map_gen.maps.danger_ores.config.coal' DOC.map_config.main_ores_rotate = nil diff --git a/map_gen/maps/danger_ores/presets/danger_ore_omnimatter_maze.lua b/map_gen/maps/danger_ores/presets/danger_ore_omnimatter_maze.lua index 8c71c883f..1a179162f 100644 --- a/map_gen/maps/danger_ores/presets/danger_ore_omnimatter_maze.lua +++ b/map_gen/maps/danger_ores/presets/danger_ore_omnimatter_maze.lua @@ -23,7 +23,7 @@ Config.player_create.starting_items = { DOC.scenario_name = 'danger-ore-omnimatter-maze' DOC.concrete_on_landfill.refund_tile = 'omnite-refined-concrete' DOC.game.technology_price_multiplier = 1 -DOC.map_config.main_ores = require 'map_gen.maps.danger_ores.compatibility.omnimatter.ores' +DOC.map_config.main_ores = require('map_gen.maps.danger_ores.compatibility.omnimatter.ores'):scale_richness(0.25) DOC.map_config.main_ores_rotate = nil DOC.map_config.no_resource_patch_shape = B.translate(B.rectangle(80), 2, 2) DOC.map_config.spawn_shape = B.translate(B.rectangle(64), 2, 2) diff --git a/map_gen/maps/danger_ores/presets/danger_ore_scrap_maze.lua b/map_gen/maps/danger_ores/presets/danger_ore_scrap_maze.lua index b64624cf5..4a1386edc 100644 --- a/map_gen/maps/danger_ores/presets/danger_ore_scrap_maze.lua +++ b/map_gen/maps/danger_ores/presets/danger_ore_scrap_maze.lua @@ -1,13 +1,15 @@ local B = require 'map_gen.shared.builders' local H = require 'map_gen.maps.danger_ores.modules.helper' local DOC = require 'map_gen.maps.danger_ores.configuration' +local Factory = require 'map_gen.maps.danger_ores.config.main_ores_factory' local Config = require 'config' local Scenario = require 'map_gen.maps.danger_ores.scenario' local ScenarioInfo = require 'features.gui.info' ScenarioInfo.set_map_name('Danger Ores - Scrapworld Maze') ScenarioInfo.add_map_extra_info([[ - This maze is covered in [entity=scrap]. + This maze is covered in [entity=scrap], with occasional dense patches of + [item=stone] for landfill. Mine it to make room for your factory, and explore the corridors to expand. ]]) @@ -19,7 +21,10 @@ Config.player_create.starting_items = { } DOC.scenario_name = 'danger-ore-scrap-maze' -DOC.map_config.main_ores = require 'map_gen.maps.danger_ores.config.scrap' +DOC.map_config.main_ore_resource_patches_config = Factory.patches() + :add_patch('stone', {scale = 1 / 32, threshold = 0.6, richness = {mult = 0.7, pow = 1.45}}) +DOC.map_config.main_ores_builder = require 'map_gen.maps.danger_ores.modules.main_ores_patches' +DOC.map_config.main_ores = require('map_gen.maps.danger_ores.config.scrap'):scale_richness(0.25) DOC.map_config.main_ores_rotate = nil DOC.map_config.no_resource_patch_shape = B.translate(B.rectangle(80), 2, 2) DOC.map_config.spawn_shape = B.translate(B.rectangle(64), 2, 2)