-- Procedural default world. Runs once, on the first launch
-- with no saved volume; after that WorldState loads the saved
-- result and this never runs again. That is why it can be
-- arbitrary Lua rather than a compact command string.
--
-- Volume is 80 x 32 x 80. Sea level 9.
--
-- Layout, looking down (x across, z down):
--    cliff plateau      volcano
--    waterfall
--    lake            spawn
--                       house

Terrain = {}

Terrain.W = 80
Terrain.H = 32
Terrain.D = 80
Terrain.SEA = 9

-- lake, an ellipse so it reaches the cliff base
Terrain.LAKE_X,  Terrain.LAKE_Z  = 22, 44
Terrain.LAKE_RX, Terrain.LAKE_RZ = 15, 17
Terrain.LAKE_FLOOR = 5

-- cliff plateau, a hard rectangle so the faces stay vertical
Terrain.CLIFF_X1, Terrain.CLIFF_X2 = 2, 34
Terrain.CLIFF_Z1, Terrain.CLIFF_Z2 = 2, 24
Terrain.CLIFF_TOP = 20

-- waterfall spills off the plateau's south face into a
-- plunge pool that merges with the lake
Terrain.FALL_X1, Terrain.FALL_X2 = 17, 20
Terrain.FALL_Z = 25
Terrain.POOL_X1, Terrain.POOL_X2 = 14, 25
Terrain.POOL_Z1, Terrain.POOL_Z2 = 25, 31
Terrain.POOL_FLOOR = 6

-- volcano
Terrain.VOLC_X, Terrain.VOLC_Z, Terrain.VOLC_R = 62, 26, 17
Terrain.VOLC_PEAK   = 27
Terrain.CRATER_R    = 4
Terrain.CRATER_FLOOR = 20
Terrain.LAVA_TOP    = 22

-- house sits on a flattened pad
Terrain.PAD_X1, Terrain.PAD_X2 = 40, 52
Terrain.PAD_Z1, Terrain.PAD_Z2 = 50, 62
Terrain.PAD_Y = 12
Terrain.HOUSE_X1, Terrain.HOUSE_X2 = 42, 50
Terrain.HOUSE_Z1, Terrain.HOUSE_Z2 = 52, 60

Terrain.hm = nil

----------------------------------------------------------------
-- helpers
----------------------------------------------------------------

function Terrain.volcDist(x, z)
  local dx, dz = x - Terrain.VOLC_X, z - Terrain.VOLC_Z
  return math.sqrt(dx*dx + dz*dz)
end

-- normalised ellipse distance: below 1 is inside
function Terrain.lakeDist(x, z)
  local dx = (x - Terrain.LAKE_X) / Terrain.LAKE_RX
  local dz = (z - Terrain.LAKE_Z) / Terrain.LAKE_RZ
  return math.sqrt(dx*dx + dz*dz)
end

function Terrain.inRect(x, z, x1, x2, z1, z2)
  return x >= x1 and x <= x2 and z >= z1 and z <= z2
end

function Terrain.index(x, z)
  return x + Terrain.W * z
end

function Terrain.heightAt(x, z)
  return Terrain.hm[Terrain.index(x, z)] or Terrain.SEA
end

----------------------------------------------------------------
-- heightmap
----------------------------------------------------------------

-- Three offset waves at different frequencies. Enough to read
-- as gentle and irregular without needing real noise.
function Terrain.baseHeight(x, z)
  local h = 12
  h = h + 2.4 * math.sin(x * 0.10) * math.cos(z * 0.085)
  h = h + 1.6 * math.sin((x + z) * 0.062)
  h = h + 1.1 * math.cos((x - z) * 0.045)
  return h
end

-- Cheap: one pass over 6400 columns, no voxel calls.
function Terrain.prepare()
  Terrain.hm = {}
  
  for z = 0, Terrain.D - 1 do
    for x = 0, Terrain.W - 1 do
      local base = Terrain.baseHeight(x, z)
      
      -- floor the hills above sea level so only the carved
      -- basins hold water
      local h = math.max(base, Terrain.SEA + 1)
      
      -- plateau, hard edged
      if Terrain.inRect(x, z, Terrain.CLIFF_X1, Terrain.CLIFF_X2,
      Terrain.CLIFF_Z1, Terrain.CLIFF_Z2) then
        h = Terrain.CLIFF_TOP
      end
      
      -- volcano cone, then crater punched into it
      local r = Terrain.volcDist(x, z)
      if r < Terrain.VOLC_R then
        local t = r / Terrain.VOLC_R
        local cone = Terrain.VOLC_PEAK - t * (Terrain.VOLC_PEAK - base)
        if cone > h then h = cone end
        if r < Terrain.CRATER_R then
          h = Terrain.CRATER_FLOOR
        end
      end
      
      -- lake basin
      local ld = Terrain.lakeDist(x, z)
      if ld < 1 then
        local basin = Terrain.LAKE_FLOOR + (Terrain.SEA - Terrain.LAKE_FLOOR) * ld
        if basin < h then h = basin end
      end
      
      -- plunge pool, carved flat so it merges with the lake
      if Terrain.inRect(x, z, Terrain.POOL_X1, Terrain.POOL_X2,
      Terrain.POOL_Z1, Terrain.POOL_Z2) then
        if Terrain.POOL_FLOOR < h then h = Terrain.POOL_FLOOR end
      end
      
      -- house pad, last so nothing else disturbs it
      if Terrain.inRect(x, z, Terrain.PAD_X1, Terrain.PAD_X2,
      Terrain.PAD_Z1, Terrain.PAD_Z2) then
        h = Terrain.PAD_Y
      end
      
      h = math.floor(h + 0.5)
      if h < 1 then h = 1 end
      if h > Terrain.H - 2 then h = Terrain.H - 2 end
      
      Terrain.hm[Terrain.index(x, z)] = h
    end
  end
end

----------------------------------------------------------------
-- column painting
----------------------------------------------------------------

function Terrain.blockAt(x, z, y, h)
  if y == 0 then return "Bedrock" end
  if y > h then return nil end
  
  local onVolcano = Terrain.volcDist(x, z) < Terrain.VOLC_R
  
  if y == h then
    if h <= Terrain.SEA then return "Sand" end
    if onVolcano and h >= 21 then return "Redsand" end
    return "Grass"
  end
  
  if onVolcano and y >= 20 then return "Redstone" end
  if h >= 19 and y > h - 4 then return "Stone" end
  if y >= h - 3 then return "Dirt" end
  return "Stone"
end

-- Paints z from z1 to z2 inclusive, clearing the full column
-- height as it goes.
--
-- The clear is explicit rather than relying on resize to
-- empty the volume. On a cold launch that assumption held
-- because the volume was new; when a world is replaced in
-- place it is just an assumption, and anything left standing
-- above the new surface would survive into the new world.
function Terrain.paintStrip(z1, z2)
  local vox = scene.voxels
  
  for z = z1, z2 do
    for x = 0, Terrain.W - 1 do
      local h = Terrain.heightAt(x, z)
      for y = 0, h do
        local name = Terrain.blockAt(x, z, y, h)
        if name then vox:set(x, y, z, name) end
      end
      for y = h + 1, Terrain.H - 1 do
        vox:set(x, y, z, 0)
      end
    end
  end
end

----------------------------------------------------------------
-- features, one per build step
----------------------------------------------------------------

function Terrain.featureList()
  return {
    Terrain.fillLake,
    Terrain.fillPool,
    Terrain.buildWaterfall,
    Terrain.fillLava,
    Terrain.buildHouse,
    Terrain.plantTrees
  }
end

function Terrain.fillLake()
  local vox = scene.voxels
  
  for z = 0, Terrain.D - 1 do
    for x = 0, Terrain.W - 1 do
      if Terrain.lakeDist(x, z) < 1 then
        local h = Terrain.heightAt(x, z)
        for y = h + 1, Terrain.SEA do
          vox:set(x, y, z, "Water")
        end
      end
    end
  end
end

function Terrain.fillPool()
  local vox = scene.voxels
  
  for z = Terrain.POOL_Z1, Terrain.POOL_Z2 do
    for x = Terrain.POOL_X1, Terrain.POOL_X2 do
      local h = Terrain.heightAt(x, z)
      for y = h + 1, Terrain.SEA do
        vox:set(x, y, z, "Water")
      end
    end
  end
end

-- A shallow pool flush with the plateau surface, then the
-- falling column down the cliff face into the plunge pool.
function Terrain.buildWaterfall()
  local vox = scene.voxels
  
  for z = Terrain.CLIFF_Z2 - 4, Terrain.CLIFF_Z2 do
    for x = Terrain.FALL_X1, Terrain.FALL_X2 do
      vox:set(x, Terrain.CLIFF_TOP, z, "Water")
    end
  end
  
  for x = Terrain.FALL_X1, Terrain.FALL_X2 do
    for y = Terrain.SEA + 1, Terrain.CLIFF_TOP do
      vox:set(x, y, Terrain.FALL_Z, "Water")
    end
  end
end

function Terrain.fillLava()
  local vox = scene.voxels
  
  for z = Terrain.VOLC_Z - Terrain.CRATER_R, Terrain.VOLC_Z + Terrain.CRATER_R do
    for x = Terrain.VOLC_X - Terrain.CRATER_R, Terrain.VOLC_X + Terrain.CRATER_R do
      if Terrain.volcDist(x, z) < Terrain.CRATER_R then
        for y = Terrain.CRATER_FLOOR + 1, Terrain.LAVA_TOP do
          vox:set(x, y, z, "Lava")
        end
      end
    end
  end
end

-- Planks walls on a plank floor, Glass Frame windows at eye
-- height, a gap for a door, brick roof with a one block
-- overhang.
function Terrain.buildHouse()
  local vox = scene.voxels
  local x1, x2 = Terrain.HOUSE_X1, Terrain.HOUSE_X2
  local z1, z2 = Terrain.HOUSE_Z1, Terrain.HOUSE_Z2
  local floor = Terrain.PAD_Y
  local wallTop = floor + 4
  
  -- floor
  for z = z1, z2 do
    for x = x1, x2 do
      vox:set(x, floor, z, "Planks")
    end
  end
  
  -- walls, hollow interior
  for y = floor + 1, wallTop do
    for z = z1, z2 do
      for x = x1, x2 do
        local edge = (x == x1 or x == x2 or z == z1 or z == z2)
        if edge then
          vox:set(x, y, z, "Planks")
        else
          vox:set(x, y, z, 0)
        end
      end
    end
  end
  
  -- windows, two per wall at eye height
  local eye = floor + 2
  local midX1, midX2 = x1 + 2, x2 - 2
  local midZ1, midZ2 = z1 + 2, z2 - 2
  
  vox:set(midX1, eye, z1, "Glass Frame")
  vox:set(midX2, eye, z1, "Glass Frame")
  vox:set(midX1, eye, z2, "Glass Frame")
  vox:set(midX2, eye, z2, "Glass Frame")
  vox:set(x1, eye, midZ1, "Glass Frame")
  vox:set(x1, eye, midZ2, "Glass Frame")
  vox:set(x2, eye, midZ1, "Glass Frame")
  vox:set(x2, eye, midZ2, "Glass Frame")
  
  -- doorway in the south wall
  local doorX = math.floor((x1 + x2) / 2)
  vox:set(doorX, floor + 1, z1, 0)
  vox:set(doorX, floor + 2, z1, 0)
  
  -- roof with overhang
  for z = z1 - 1, z2 + 1 do
    for x = x1 - 1, x2 + 1 do
      vox:set(x, wallTop + 1, z, "Red Brick")
    end
  end
  
  vox:set(x1 + 2, floor + 1, z2 - 2, "Crafting Table")
end

-- Hand placed rather than scattered, so none land in the
-- lake, the crater or the house.
function Terrain.plantTrees()
  local spots = {
    {52, 48}, {58, 54}, {36, 66}, {44, 70},
    {30, 70}, {66, 62}, {12, 60}, {70, 48}
  }
  
  for _, s in ipairs(spots) do
    Terrain.tree(s[1], s[2])
  end
end

function Terrain.tree(x, z)
  local vox = scene.voxels
  local h = Terrain.heightAt(x, z)
  if h <= Terrain.SEA then return end
  
  local trunkTop = h + 4
  for y = h + 1, trunkTop do
    vox:set(x, y, z, "Wood")
  end
  
  for dy = -1, 2 do
    local r = 2
    if dy == 2 then r = 1 end
    for dz = -r, r do
      for dx = -r, r do
        if math.abs(dx) + math.abs(dz) + math.abs(dy) <= r + 1 then
          local ly = trunkTop + dy
          if ly < Terrain.H and not (dx == 0 and dz == 0 and dy < 2) then
            vox:set(x + dx, ly, z + dz, "Leaves")
          end
        end
      end
    end
  end
end

-- Flat plain: a constant heightmap, so paintStrip and
-- blockAt need no special casing -- the same column logic
-- yields grass over dirt over stone. Used with an empty
-- feature list, so no lake, volcano, house or trees.
function Terrain.prepareFlat(h)
  h = h or 11
  Terrain.hm = {}
  for z = 0, Terrain.D - 1 do
    for x = 0, Terrain.W - 1 do
      Terrain.hm[Terrain.index(x, z)] = h
    end
  end
end
