###############################################################################
# layout_caves.des: Layouts based on cavelike formations, largely using
#                   procedural noise functions
###############################################################################

: crawl_require("dlua/util.lua")
: crawl_require("dlua/layout/procedural.lua")
: crawl_require("dlua/layout/zonify.lua")
: crawl_require("dlua/layout/hyper.lua")
: crawl_require("dlua/layout/hyper_caves.lua")
: crawl_require("dlua/layout/theme.lua")
: crawl_require("dlua/layout/minimum_map_area.lua")

##############################################################
# layout_layer_cave
#
# A similar idea to layered_grids - pick two or three different noise
# functions from weighted selections and draw them overwritten.
#
NAME:   layout_layer_cave
DEPTH:  D:4-, Lair, Snake
WEIGHT: 5, 15 (Snake)
ORIENT: encompass
TAGS:   overwritable layout allow_dup unrand layout_type_narrow_caves
{{
  local gxm,gym = dgn.max_bounds()

  -- First pass
  -- These should be sprawling corridors that will connect up the
  -- bigger features of the second pass
  local limit = 0.6
  -- TODO: Implement these functions as rooms and use hyper so we
  -- can automatically get randomised size/position and door carving
  local pweights = {
    -- Cavernous areas
--    { weight = 10, func = function()
  --      limit = 0.51
    --    return procedural.simplex3d { scale = 1 }
      --end },
    -- Twisty paths
    { weight = 10, func = function()
        limit = 0.2
        return procedural.abs(procedural.simplex3d { scale = util.random_range_real(.5,1), unit = false })
      end },
    -- Angular paths
    { weight = 10, func = function()
        limit = 0.3
        return procedural.worley_diff { scale = util.random_range_real(.2,.4) }
      end },
  }

  local primary = procedural.add(
    util.random_weighted_from("weight",pweights).func(),
    procedural.mul(procedural.border { padding = 8 }, limit)
  )

  procedural.render_map(_G,primary,function(v) return v < limit and '.' or 'x' end)

  -- Second pass
  limit = 0.2
  local sweights = {
    { weight = 10, func = nil },
    -- Large blobby rooms
    { weight = 10, func = function()
        limit = util.random_range_real(0.2,0.4)
        return procedural.simplex3d { scale = util.random_range_real(.2,.4) }
      end },
    -- More angular rooms
    { weight = 10, func = function()
        limit = 0.5
        return procedural.sub(1,procedural.worley_diff { scale = util.random_range_real(.02,.1) } )
      end },
    -- A big central cavern
    { weight = 10, func = function()
        limit = 0.8
        return procedural.add(
          procedural.distance { origin = { x = gxm/2, y = gym/2 }, radius = math.min(gxm,gym)/3 },
          procedural.worley_diff { scale = util.random_range_real(.4,.8) })
      end },
  }
  local schoice = util.random_weighted_from("weight",sweights)
  -- Add or remove?
  -- TODO: It produces some very cool variations but sometimes
  -- results in very small levels. These are still fairly interesting though,
  -- is it a bad thing? (Should maybe use a separate negative pass anyway)
  local glyph = crawl.coinflip() and 'x' or '.'
  if schoice.func ~= nil then
    local secondary = procedural.add(
      schoice.func(),
      procedural.mul(procedural.border { padding = 8 }, limit)
    )
    procedural.render_map(_G,secondary,function(v) return v < limit and glyph or nil end)
  end

  zonify.map_fill_zones(_G, 1, 'x')

  theme.D.caves(_G)
}}
# Enforce minimum floor size - otherwise we get very tiny floors sometimes
validate {{
  return minimum_map_area.is_map_big_enough(_G, minimum_map_area.NARROW_CAVES)
}}
MAP
ENDMAP

##############################################################
# layout_cave_shapes
#
# The premise here is we pick a random shape function and apply cavelike edges.
# We optionally apply a fairly large-scale domain distortion to the underlying shape.
# TODO: * Some flooding/river on 1/10 chance (certain branches)
#       * More shape variety
#       * Some shapes work better in certain branches. Snake/D could occasionally get straight edged shapes.
#       * Snake should have domain distortion every time (use the one from Halls)
#       * It should be possible to fold most of these effects into layer_cave
#
NAME:   layout_cave_shapes
DEPTH:  Lair, Snake, Depths
WEIGHT: 3 (Lair), 5 (Snake), 10 (Depths)
ORIENT: encompass
TAGS:   overwritable layout allow_dup unrand layout_type_open_caves
{{
  if is_validating() then return; end
  local gxm,gym = dgn.max_bounds()
  local width = crawl.random_range(10,20)
  -- TODO: A lot of these functions should be moved into an e.g. procedural.complex namespace
  local function fcircle()
    return procedural.distance{ origin = { x = gxm/2, y = gym/2 }, radius = math.min(gxm,gym)/2-2 }
  end
  local function fcross()
    return procedural.sub(1,procedural.max(
      procedural.bar { position = gym/2,width = width,inner = width/2 },
      procedural.bar { position = gxm/2,width = width,inner = width/2,horizontal = false } ) )
  end
  local shapes = {
    -- Circle
    { weight = 12, shape = function()
        local circle = fcircle()
        return function(x,y)
          return procedural.minmax_map(circle(x,y),0.5,1.0)
        end
      end},
    -- Cross
    { weight = 5, shape = fcross },
    -- Donut
    { weight = 8, shape = function()
        local outer_r = util.random_range_real(0.6,1.0)
        local circle = fcircle()
        local cwidth = util.random_range_real(0.3,0.5)
        local fdonut = function(x,y)
          return 1-procedural.boundary_map(circle(x,y),outer_r-cwidth,outer_r-cwidth*2/3,outer_r-cwidth/3,outer_r)
        end
        -- Optionally draw a bar
        if crawl.x_chance_in_y(2,3) then
          -- With one of two methods
          local fbar
          if crawl.coinflip() then
            local horiz = crawl.coinflip()
            local fbar1 = procedural.bar { horizontal = horiz, position = (horiz and gym or gxm)/2, width = width, inner = width/2 }
            fbar = function(x,y)
              local v = 1 - fbar1(x,y)
              if circle(x,y) > 1 then v = v + (circle(x,y)-1)*10 end
              return v
            end
          else
            local frad = procedural.radial{ phase = crawl.random_real(), origin = { x = gxm/2, y = gym/2 } }
            local range = util.random_range_real(0.04,0.2)
            fbar = function(x,y)
              local r = frad(x,y)
              local v = 1 - procedural.boundary_map(r,0,range/3,2*range/3,range) - procedural.boundary_map(r,0.5,0.5+range/3,0.5+2*range/3,0.5+range)
              -- Cut off outside the circle
              if circle(x,y) > 1 then v = v + (circle(x,y)-1)*20 end
              return v
            end
          end
          -- Since we're adding a bar, 2/3 chance for a central chamber
          if crawl.x_chance_in_y(2,3) then
            local fdonut1 = fdonut
            fdonut = function(x,y)
              return fdonut1(x,y) - procedural.boundary_map(circle(x,y),0.05,0.05+cwidth/3,0.05+cwidth*2/3,0.05+cwidth)
            end
          end
          fdonut = procedural.mul(fdonut,fbar)
        end
        return fdonut
      end },
    -- Simplex chunks
    { weight = 20, shape = function()
        local fsimp = procedural.simplex3d{ scale = util.random_range_real(0.1,0.8) }
        local fmul = util.random_range_real(2,3)
        return function(x,y)
          return fsimp(x,y)*fmul - 1
        end
      end
    }
  }
  local shape = util.random_weighted_from("weight",shapes).shape()

  if (crawl.coinflip()) then
    -- Apply domain distortion
    shape = procedural.distort {source = shape,
      scale = crawl.random_range(5,20),
      offsetx = procedural.simplex3d{ scale = util.random_range_real(0.01,0.2) },
      offsety = procedural.simplex3d{ scale = util.random_range_real(0.01,0.2) } }
  end
  local fcave
  if crawl.coinflip() then
    local fsim = procedural.simplex3d{ scale = util.random_range_real(0.8,2) }
    fcave = function(x,y) return math.abs(fsim(x,y)*2-1) end
  else
    -- Scales between .1 and about 8 are good (but .1-.5 most defined)
    -- TODO: For this random_range (and actually a lot of them) a log scale
    -- or anything that tends to lower vales would be beneficial since that's
    -- where all the interesting features happen.
    fcave = procedural.worley_diff{ scale = util.random_range_real(.3,.8) }
  end
  local fdraw = procedural.add(shape,procedural.border{padding=5},fcave)
  local limit = util.random_range_real(0.65,0.9)

  procedural.render_map(_G,fdraw,function(value) return value < limit and '.' or 'x' end)
  zonify.map_fill_zones(_G, 1)

  theme.D.caves(_G)
}}

##############################################################
# layout_falls_marsh
#
# Use polar inverse on a noise function to create corridors that
# tail off in size, with dripping splatters in an enclosed layout.
# Then add pools like layout_cave_pools (but also including "pools"
# of trees and stone).
#
# TODO: Somebody with greater noise manipulation prowess than I
# should widen the occasional thin corridor at the end of each drip.
#
NAME:   layout_falls_marsh
DEPTH:  Swamp, !Swamp:$
WEIGHT: 10
ORIENT: encompass
TAGS:   overwritable layout allow_dup unrand no_pool_fixup layout_type_swamp
{{
  if is_validating() then return end

  local gxm,gym = dgn.max_bounds()
  local limit,border,margin = 0.35,16,16
  local reps = crawl.random_range(70,85)

  local fcaves = procedural.worley_diff { scale = 7.5/reps }
  fcaves = procedural.transform.polar_inverse(fcaves,gym/1.8,100*gxm/reps,gym/2)
  fcaves = procedural.add(fcaves,procedural.mul(limit,procedural.border{padding=border,margin=margin}))

  procedural.render_map(_G,fcaves,function(value) return value < limit and '.' or 't' end)
  zonify.map_fill_zones(_G, 1)

  local pool_contents = {}
  pool_contents["w"] = 75 + you.depth() * 10
  pool_contents["W"] = 60 - you.depth() * 10
  pool_contents["c"] = 5 + you.depth() * 5
  pool_contents["t"] = 10
  add_pools { replace = ".",
              pool_size = 8 + crawl.random2(8),
              contents = pool_contents,
              frequency = pool_frequency }

  subst('W = W:75 t')
  subst('w = W:75 t')
  subst('c = .:570 w t')
  subst('. = .:' .. 71 - you.depth() * 4 .. ' W:' .. 29 + you.depth() * 4)
  subst('x = t')

  remove_isolated_glyphs { find = "W", percent = 80 }
  zonify.grid_fill_water_zones(1,"t")
}}
# Enforce minimum floor size - otherwise we get very tiny floors sometimes
validate {{
  -- If we try to validate the map before without creating it,
  --  we get a crash at start
  if is_validating() then return end
  return minimum_map_area.is_map_big_enough(_G, minimum_map_area.SWAMP, ".W")
}}

##############################################################
# layout_cave_pods
#
# Intended as a series of interconnected roughly geometrically-shaped rooms
# for e.g. Dis/Zot, came out much more organic like a burrow of caves.
#
# TODO: Wider the paths, then add to Spider at weight 10.
#
NAME:   layout_cave_pods
DEPTH:  D:4-, Lair, Snake
WEIGHT: 5, 15 (Lair)
ORIENT: encompass
TAGS:   overwritable layout allow_dup unrand layout_type_open_caves
{{
  if is_validating() then return; end
  local gxm,gym = dgn.max_bounds()

  -- TODO: Add some branch-appropriate domain distortion / water
  -- TODO: Randomise scales of rooms and corridors (need to scale
  -- corridor width up and down with noise scale too otherwise they end up hugely wrong size)
  local frooms = procedural.worley_diff{ scale = 0.15 }
  local fcorridors = procedural.worley_diff{ scale = 0.1 }

  if you.in_branch("Snake") then
    frooms = procedural.transform.damped_distortion(frooms)
    fcorridors = procedural.transform.damped_distortion(fcorridors)
  end

  local fhalls = procedural.sub(fcorridors,procedural.mul(frooms,0.5))
  local fbord = procedural.border{ padding = 4,additive = true }
  frooms = procedural.sub(frooms,fbord)
  fcorridors = procedural.add(fcorridors,procedural.mul(fbord,0.3))

  procedural.render_map(_G, fcorridors, function(v) return (v < 0.08 and '.' or 'x') end)
  procedural.render_map(_G, frooms, function(v) return (v > 0.4 and '.' or nil) end)

  zonify.map_fill_zones(_G, 1, 'x')
}}
MAP
ENDMAP

##############################################################
# layout_chambers
#
# This is a very similar version of the previous one and
# should really be folded in with randomisation
#
NAME:   layout_chambers
DEPTH:  Snake, Spider, Pan
WEIGHT: 15 (Snake), 5 (Spider), 10 (Pan)
ORIENT: encompass
TAGS:   overwritable layout allow_dup unrand layout_type_open_caves
{{
  if is_validating() then return; end
  local gxm,gym = dgn.max_bounds()

  local frooms = procedural.worley_diff{ scale = 0.2 }
  local fcorridors = procedural.worley_diff{ scale = 0.3 }
  local fhalls = procedural.sub(fcorridors,procedural.mul(frooms,0.5))
  local fbord = procedural.border{ padding = 4,additive = true }
  frooms = procedural.sub(frooms,fbord)
  fcorridors = procedural.add(fcorridors,procedural.mul(fbord,0.3))

  procedural.render_map(_G, fcorridors, function(v) return (v < .15 and '.' or 'x') end)
  procedural.render_map(_G, frooms, function(v) return (v > 0.3 and '.' or nil) end)

  zonify.map_fill_zones(_G, 1, 'x')
}}
MAP
ENDMAP

##############################################################
# layout_cave_town
#
# A big cavern with a few huts or caves in it.
#
# TODO: Use other layouts as the base (e.g. cave_shapes, a
#       river/forest/pools layout like bigger_room, even
#       swamp / shoals) and add rooms to them
#
# I don't like this TODO.  In my mind, this layout makes a great
# distinguishing feature for Snake. ~infiniplex
#
NAME:   layout_cave_town
DEPTH:  Snake:2-
WEIGHT: 5 (Snake:2), 15 (Snake:3), 50 (Snake:4)
ORIENT: encompass
TAGS:   overwritable layout allow_dup unrand no_pool_fixup layout_type_city
TAGS:   no_rotate no_hmirror no_vmirror
{{
  if is_validating() then return; end

  -- Similar size calculations to chaotic_city
  local function room_size_func()
    return { x = 5 + crawl.random2(7) + crawl.random2(5),y = 5 + crawl.random2(7) + crawl.random2(5) }
  end
  local function room_size_func_small()
    return { x = 4 + crawl.random2(4) + crawl.random2(3),y = 4 + crawl.random2(4) + crawl.random2(3) }
  end
  local function room_size_func_big()
    return { x = 7 + crawl.random2(9) + crawl.random2(7),y = 7 + crawl.random2(9) + crawl.random2(7) }
  end

  local chosen,params = hyper.caves.random_cave_room()
  local transform = not params.wall and hyper.rooms.add_walls or nil

  local is_random = false -- crawl.coinflip()

  local gxm,gym = dgn.max_bounds()
  hyper.build_layout(_G,{
    name = "Cave Town",
    no_windows = true,
    build_fixture = {
      { pass = "Primary", type = "build", strategy = hyper.place.strategy_primary, max_rooms = 1, generators = {
        { generator = "code", paint_callback = hyper.caves.cavern_walls, analyse = false,
          min_size_x = gxm-12, max_size_x = gxm-2, min_size_y = gym-12, max_size_y = gym-2, place_padding = 1 },
        { generator = "code", paint_callback = hyper.caves.cave_walls, analyse = false,
          min_size_x = gxm-12, max_size_x = gxm-2, min_size_y = gym-12, max_size_y = gym-2, place_padding = 1 }
      } },
      { pass = "Rooms1", type = "build", strategy = hyper.place.strategy_open, max_rooms = crawl.random_range(1,3), generators = {
        { generator = "code", class = "room", weight = 5, paint_callback = chosen, size_callback = room_size_func_big, room_transform = transform },
      } },
      { pass = "Rooms2", type = "build", strategy = is_random and hyper.place.strategy_random or hyper.place.strategy_open,
        max_rooms = is_random and crawl.random_range(12,18) or crawl.random_range(8,12), generators = {
        { generator = "code", class = "room", weight = 15, paint_callback = chosen, size_callback = room_size_func, room_transform = transform },
        { generator = "code", class = "room", weight = 10, paint_callback = chosen, size_callback = room_size_func_small, room_transform = transform },
      } }
    }
  })
  zonify.grid_fill_zones(1,"rock_wall")
  theme.D.caves(_G)
}}
MAP
ENDMAP

##############################################################
# layout_cocytus_water_paths
#
# Previously named layout_gehenna_lava_paths, had lava instead
#  of water, and appeared in Gehenna.
#
NAME:   layout_cocytus_water_paths
DEPTH:  Coc
WEIGHT: 10
ORIENT: encompass
TAGS:   overwritable layout allow_dup unrand layout_type_open_caves
{{
  if is_validating() then return end

  local gxm,gym = dgn.max_bounds()
  extend_map { width = gxm, height = gym, fill = 'x' }

  -- Set a scale to produce some nice-sized chunks
  local depth_fraction = you.depth_fraction()
  local scale      = 0.95 + crawl.random_real() * 0.25 - depth_fraction * 0.35
  local path_break = 0.15 + crawl.random_real() * 0.05 + depth_fraction * 0.05
  local wall_break = 0.8  - crawl.random_real() * (0.6 - path_break)

  -- between outmost paths and outmost water
  local padding = 8 + crawl.random_real() * 6 + depth_fraction * 6

  -- Setup a worley function
  local worley1 = procedural.worley_diff { scale = scale/4 }
  if (scale > 1.5 and crawl.coinflip()) then
    -- Distort
    worley1 = procedural.distort {
      source = worley1,
      offsetx = procedural.simplex3d { scale = scale*(util.random_range_real(0.2,0.8)) }, -- was 1.8
      offsety = procedural.simplex3d { scale = scale*(util.random_range_real(0.2,0.8)) },
      scale = util.random_range_real(1,3)/scale
    }
  end

  for x = 1,gxm-2,1 do
    for y = 1,gym-2,1 do
      local val = worley1(x,y) + 1
                 - procedural.boundary_map(x,1,padding,gxm-2-padding,gxm-2)/2
                 - procedural.boundary_map(y,1,padding,gym-2-padding,gym-2)/2
      if (val) > wall_break then
        mapgrd[x][y] = "x"
      elseif (val) > path_break then
        mapgrd[x][y] = "w"
      else
        mapgrd[x][y] = "."
      end
    end
  end

  -- we want to fill flight-accessible areas with water and all
  --  closets with rock
  zonify.map_fill_zones(_G, 1, 'w')
  nsubst(". = 1:@ / *:.")
  fill_disconnected{wanted = '@', fill = 'x'}
}}
MAP
ENDMAP

##############################################################
# layout_gehenna_lava_caves
#
# Basically perlin noise, with rock and lava at the extreme
#  values.  The middle values become the paths.
#
NAME:   layout_gehenna_lava_caves
DEPTH:  Geh
WEIGHT: 20
ORIENT: encompass
TAGS:   overwritable layout allow_dup unrand layout_type_narrow_caves
{{
  if is_validating() then return end

  local gxm,gym = dgn.max_bounds()
  extend_map { width = gxm, height = gym, fill = 'x' }

  -- choose some parameters based on depth
  local depth_fraction = you.depth_fraction()
  local scale      = 1.6 - crawl.random_real() * 0.3 - depth_fraction * 0.4
  local padding    = crawl.random_range(6, 10)
  local wall_break = 0.05 + crawl.random_real() * 0.05 + depth_fraction * 0.1
  local lava_break = 0.15 + crawl.random_real() * 0.1  + depth_fraction * 0.15
  local open_break = 0.55 + crawl.random_real() * 0.05 + depth_fraction * 0.15

  -- set up the main perlin noise function
  local perlin1 = procedural.simplex3d { scale = scale }
  if crawl.coinflip() then
    perlin1 = procedural.distort {
      source = perlin1,
      offsetx = procedural.simplex3d { scale = util.random_range_real(1,2) },
      offsety = procedural.simplex3d { scale = util.random_range_real(1,2) },
      scale = util.random_range_real(0.5,1)/scale
    }
  end

  -- set up the edges for the map
  local boundary = 8
  local min_hard     = boundary
  local min_padded   = boundary + padding
  local max_padded_x = gxm - 1 - boundary - padding
  local max_hard_x   = gxm - 1 - boundary
  local max_padded_y = gym - 1 - boundary - padding
  local max_hard_y   = gym - 1 - boundary

  -- render the map
  for x = 1,gxm-2,1 do
    for y = 1,gym-2,1 do
      local val = perlin1(x, y)
                  * procedural.boundary_map(x, min_hard,     min_padded,
                                               max_padded_x, max_hard_x)
                  * procedural.boundary_map(y, min_hard,     min_padded,
                                               max_padded_y, max_hard_y)
      if val < wall_break then
        mapgrd[x][y] = "x"
      elseif val < lava_break then
        mapgrd[x][y] = "l"
      elseif val < open_break then
        mapgrd[x][y] = "."
      else
        -- leave as 'x'
      end
    end
  end

  -- assorted fixups
  theme.add_gehenna_buildings(_G)
  zonify.map_fill_zones(_G, 1, 'l')
  zonify.map_fill_lava_zones(_G, 1, 'x')
}}
MAP
ENDMAP

##############################################################
# layout_ice_bergs
#
NAME:   layout_ice_bergs
DEPTH:  Coc
WEIGHT: 10
ORIENT: encompass
TAGS:   overwritable layout allow_dup unrand layout_type_open_caves
{{
  if is_validating() then return; end
  local gxm,gym = dgn.max_bounds()

  -- There is precedent for requiring flight in Coc, so could have
  -- a chance that not all bergs will get connected up
  -- Disabled until it's implemented better
  local connect_up = true -- crawl.x_chance_in_y(5,6)
  local nilresult = 'w'
  if connect_up then
    local fcorridors = procedural.worley_diff{ scale = 0.15 }
    procedural.render_map(_G, fcorridors, function(v) return (v < .1 and 'W' or nilresult) end)
    nilresult = nil
  end

  local fbord = procedural.border{ padding = 4,additive = true }
  local fcave = procedural.add(fbord,procedural.worley_diff { scale = 0.2 })
  procedural.render_map(_G, fcave, function(v) return (v >= 0.8 and 'x' or nilresult) end)

  local fbergs = procedural.worley_diff{ scale = 0.2 }
  fbergs = procedural.sub(fbergs,fbord)
  procedural.render_map(_G, fbergs, function(v)
    if v > 0.6 then return "x"
    elseif v > 0.2 then return "."
    else return nil end
    -- return (v > 0.3 and '.' or nil)
  end)

  if connect_up then
    zonify.map_fill_zones(_G, 1, 'x')
  end
}}
MAP
ENDMAP
