File: v_paint.lua

package info (click to toggle)
crawl 2%3A0.33.1-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 95,264 kB
  • sloc: cpp: 358,145; ansic: 27,203; javascript: 9,491; python: 8,359; perl: 3,327; java: 2,667; xml: 2,191; makefile: 1,830; sh: 611; objc: 250; cs: 15; sed: 9; lisp: 3
file content (336 lines) | stat: -rw-r--r-- 12,191 bytes parent folder | download | duplicates (7)
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
------------------------------------------------------------------------------
-- v_paint.lua:
--
-- Functions for painting the Vaults layouts and managing
-- walls and areas that rooms can be applied to.
--
-- We will (in the simplest case) paint rectangles of floor onto the dungeon
-- and then sweep across the whole grid to determine which squares can be
-- used for what.
--
-- Whilst placing rooms we'll update the usage grid as areas become available
-- or new walls become eligible (but that's a much more predictable case than
-- initial level generation).
------------------------------------------------------------------------------

------------------------------------------------------------------------------
-- Initialize and empty usage grid
local function new_layout(width, height)
  layout_grid = { width = width, height = height }

  for y = 0, (height - 1), 1 do
    layout_grid[y] = { }
    for x = 0, (width - 1), 1 do
      layout_grid[y][x] = { solid = true } -- 1 means wall
    end
  end
  return layout_grid
end
function vaults_new_layout(width, height)
  return new_layout(width,height)
end
local function new_usage(width, height)
  usage_grid = { eligibles = { }, width = width, height = height }

  for y = 0, (height-1), 1 do
    usage_grid[y] = { }
    for x = 0, (width-1), 1 do
      usage_grid[y][x] = { usage = "none" }
    end
  end
  return usage_grid
end

local function get_usage(usage_grid,x,y)
  -- Handle out of bounds
  if usage_grid[y] == nil then
    return nil
  end
  if usage_grid[y][x] == nil then
    return nil
  end

  return usage_grid[y][x]
end

-- Global version
function vaults_get_usage(usage_grid,x,y)
  return get_usage(usage_grid,x,y)
end

local function set_usage(usage_grid,x,y,usage)
  if usage_grid[y] == nil or usage_grid[y][x] == nil then return false end
  -- Check existing usage, remove it from eligibles if it's there
  local current = usage_grid[y][x]
  if current.eligibles_index ~= nil then
    table.remove(usage_grid.eligibles,current.eligibles_index)
  end
  -- Add to the eligibles list if it's eligible
  if usage.usage == "eligible" or usage.usage == "eligible_open" or usage.usage == "open" then
    usage.spot = { x = x, y = y } -- Store x,y in the usage object otherwise when we look it up in the list we don't know where it came from!
    table.insert(usage_grid.eligibles,usage)
    usage.eligibles_index = #(usage_grid.eligibles)  -- Store index of the new item so we can remove it when it's overwritten
  end
  -- Store usage in grid
  usage_grid[y][x] = usage
end

-- Global version
function vaults_set_usage(usage_grid,x,y,usage)
  return set_usage(usage_grid,x,y,usage)
end

local function get_layout(layout_grid,x,y)
  -- Handle out of bounds
  if layout_grid[y] == nil then
    return { space = true, solid = true, exit = false, feature = "permarock_wall" }
  end
  if layout_grid[y][x] == nil then
    return { space = true, solid = true, exit = false, feature = "permarock_wall" }
  end

  return layout_grid[y][x]
end

function vaults_get_layout(layout_grid,x,y)
  return get_layout(layout_grid,x,y)
end

local function set_layout(layout_grid,x,y,value)
  if layout_grid[y] == nil or layout_grid[y][x] == nil then
  else
    layout_grid[y][x] = value
  end
end
function vaults_set_layout(layout_grid,x,y,value)
  return set_layout(layout_grid,x,y,value)
end

local function determine_usage_from_layout(layout_grid,options)

  usage_restricted_count = 0
  usage_open_count = 0
  usage_eligible_count = 0
  usage_none_count = 0

  local gxm, gym = layout_grid.width,layout_grid.height
  local usage_grid = new_usage(gxm,gym)

  local function gridsum(grid,list)
    local sum = 0
    for i,pos in ipairs(list) do
      if grid[pos.y][pos.x].solid then sum = sum + 1 end
    end
    return sum
  end

  local local_grid = { }
  for yl = -options.min_distance_from_wall, options.min_distance_from_wall, 1 do
    local_grid[yl] = { }
  end

  local adjacent_vectors = { {x=-1,y=0},{x=1,y=0},{x=0,y=-1},{x=0,y=1} }
  local diagonal_vectors = { {x=-1,y=-1},{x=1,y=-1},{x=-1,y=1},{x=1,y=1} }

  for x = 0, gxm-1, 1 do
    for y = 0, gym-1, 1 do
      -- We need to know the local layout grid around this square
      -- This flag will track if there is only floor in the area
      local only_floor = true
      for yl = -options.min_distance_from_wall, options.min_distance_from_wall, 1 do
        for xl = -options.min_distance_from_wall, options.min_distance_from_wall, 1 do
          local cell = get_layout(layout_grid,x + xl,y + yl)
          local_grid[yl][xl] = cell
          if cell.solid then only_floor = false end
        end
      end

      -- Completely open floor so we could place a room here
      if only_floor == true then
        set_usage(usage_grid,x,y, { usage = "open" })
      else

        -- Are we dealing with floor or wall?
        if local_grid[0][0].solid then -- Wall

          -- A wall can either be usage "none" meaning parts of a room could later be built over it;
          -- or it can be "eligible" meaning it can be used as a connecting wall/door to a room;
          -- or it can be "restricted" meaning its geometry is not suited for a connecting wall or it has already been used

          -- We don't need to cover all cases of complex geometry since for now the layouts will be
          -- making simple large blocks. If complex geometry ever gets used it doesn't matter if we flag some squares
          -- as eligible when they don't really work because room placement will still vetoe if it can't find a clear area of
          -- open or none. It's more important to find all squares that *could* be eligible.

          -- Sum the adjacent squares
          local adjacent_sum = gridsum(local_grid, adjacent_vectors)
          -- Eligible squares have floor on only one side
          -- This ignores diagonals (which is where complex geometry will produce some eligible squares that aren't
          -- really eligible). But it's complicated because we're after diagonals only on the side where the floor is.

          -- What we need to know in the usage grid is the normal, i.e. the direction in which the user will be entering
          -- the room.
          if adjacent_sum == 3 then
           -- Floor to the north
            if not local_grid[-1][0].solid then
              set_usage(usage_grid,x,y, { usage = "eligible", normal = adjacent_vectors[4], depth = 1})
            end
            -- Floor to the south
            if not local_grid[1][0].solid then
              set_usage(usage_grid,x,y, { usage = "eligible", normal = adjacent_vectors[3], depth = 1})
            end
            -- Floor to the west
            if not local_grid[0][-1].solid then
              set_usage(usage_grid,x,y, { usage = "eligible", normal = adjacent_vectors[2], depth = 1})
            end
            -- Floor to the east
            if not local_grid[0][1].solid then
              set_usage(usage_grid,x,y, { usage = "eligible", normal = adjacent_vectors[1], depth = 1})
            end
          else
            -- Wall all around?
            if adjacent_sum == 4 then

              local diagonal_sum = gridsum(local_grid, diagonal_vectors)

              -- Wall mostly all around? (We allow one missing corner otherwise rooms can't overlap corners
              -- and logically it's fine for any wall to be placed there, other missing holes will fail the placement
              -- anyway)
              if diagonal_sum >= 3 then
                -- Should have been set this way at initialization but let's check anyway
                set_usage(usage_grid,x,y, { usage = "none" })
              else
                -- There are some diagonal holes so we can't use this square
                set_usage(usage_grid,x,y, { usage = "restricted" })
              end

            end
          end

        else -- Floor

          -- We already know there is a wall nearby, so this square is restricted
          set_usage(usage_grid,x,y, { usage = "restricted", reason = "border" })
        end
      end
    end
  end
  return usage_grid
end

-- Determine if a point is inside an oval
-- TODO: There's something wrong with this equation, ovals are coming out with flattened sides and sometimes completely square.
--       Needs somehow adjusting for the fact that we're dealing with grid squares, if that's indeed the problem ...
local function inside_oval(x,y,item)
  -- Radii
  local rx = (item.corner2.x - item.corner1.x)/2
  local ry = (item.corner2.y - item.corner1.y)/2
  -- Center point
  local h = (item.corner2.x + item.corner1.x)/2
  local k = (item.corner2.y + item.corner1.y)/2
  -- Test
  return ((math.pow(x-h,2)/math.pow(rx,2) + math.pow(y-k,2)/math.pow(ry,2))<=1)
end

local function inside_trapese(x,y,item)

  local width1 = item.width1
  local width2 = item.width2
  if width1 == nil then width1 = 0 end
  if width2 == nil then width2 = 1 end

  return true

end

function paint_grid(paint, options, grid)

  for i,item in ipairs(paint) do
    local feature
    if item.type == "floor" then
      feature = options.layout_floor_type
    elseif item.type == "wall" then
      feature = options.layout_wall_type
    elseif item.type == "space" then
      feature = "space"
    elseif item.feature ~= nil then
      feature = item.feature
    end

    -- Check which shape to paint
    local shape_type = "quad"
    if item.shape ~= nil then shape_type = item.shape end

    -- Discover whether the feature type is solid
    local space = (feature == "space")
    local solid = not space and not feat.has_solid_floor(feature)
    local empty = (item.empty ~= nil and item.empty)
    -- Paint features onto grid
    if shape_type == "quad" or shape_type == "ellipse" or shape_type == "trapese" then
      -- TODO: shape types are begging to be modularised
      -- Set layout details in the painted area
      for x = item.corner1.x, item.corner2.x, 1 do
        for y = item.corner1.y, item.corner2.y, 1 do
          if shape_type == "quad" or (shape_type == "ellipse" and inside_oval(x,y,item)) or (shape_type == "trapese" and inside_trapese(x,y,item)) then
            local ax,ay = x,y
            if item.wrap then
              ax = x % grid.width
              ay = y % grid.height
            end

            set_layout(layout_grid,ax,ay, { solid = solid, feature = feature, space = space, empty = empty })
          end
        end
      end
    elseif shape_type == "plot" then
      if item.points ~= nil then
        for i,pos in ipairs(item.points) do
          set_layout(layout_grid, pos.x, pos.y, { solid = solid, feature = feature, empty = empty })
        end
      else
        set_layout(layout_grid, item.x, item.y, { solid = solid, feature = feature, empty = empty })
      end
    end

  end

end

function paint_vaults_layout(paint, options, layout_grid)

  -- Default options
  if options == nil then options = vaults_default_options() end

  -- Pick wall type from spread in config
  local wall_type = "stone_wall"
  if options.wall_type ~= nil then wall_type = options.wall_type end
  if options.layout_wall_weights ~= nil then
    local chosen = util.random_weighted_from("weight", options.layout_wall_weights)
    wall_type = chosen.feature
  end
  -- Store it in options so it can be used for room surrounds also
  options.layout_wall_type = wall_type

  local gxm, gym = dgn.max_bounds()
  layout_grid = new_layout(gxm,gym) -- Will contain data about how each square is used and therefore how rooms can be applied
  paint_grid(paint,options,layout_grid) -- Paint fills onto the layout grid

  if _VAULTS_DEBUG then dump_layout_grid(layout_grid) end

  local usage_grid = determine_usage_from_layout(layout_grid,options) -- Analyse the layout to determine usage

  -- Apply to the actual dungeon grid
  for x = 0, gxm-1, 1 do
    for y = 0, gym-1, 1 do
      local cell = get_layout(layout_grid,x,y)
      if cell.feature ~= nil and cell.feature ~= "space" then
        dgn.grid(x,y,cell.feature)
      elseif cell.feature == nil then
        -- Make sure we set the right type of wall in unpainted grids
        dgn.grid(x,y,wall_type)
      end
    end
  end
  return usage_grid

end