File: hyper_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 (303 lines) | stat: -rw-r--r-- 12,772 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
------------------------------------------------------------------------------
-- hyper_paint.lua:
--
-- Functions for painting onto usage grids (for whole layouts and for
-- smaller code rooms).
--
-- TODO: Loads of this file is redundant with procedural primitives. Really
-- we just need some sensible mappings of procedurals to paint shapes and
-- it'll be a lot more flexible.
------------------------------------------------------------------------------

hyper.paint = {}

function hyper.paint.paint_grid(paint, options, usage_grid)

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

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

    -- Get information about the feature
    local feature,space,solid,wall = hyper.paint.feature_flags(feature_type,options)
    local feature_usage = item.usage

    -- These three flags are in addition to the feature and affect how the builder treats the squares during
    -- placement and subsequently.
    local open = (item.open ~= nil and item.open) -- Further rooms can be placed in this area. Be careful (for now) to leave a border around this so as not to block connections
    local exit = (item.exit ~= nil and item.exit) -- Same as using @ in vaults, allows this square to have connections adjacent, blocks any non-exit squares from being used as such

    if item.corner1 == nil then item.corner1 = { x = 0, y = 0 } end
    if item.corner2 == nil then item.corner2 = { x = usage_grid.width-1, y = usage_grid.height-1 } end

    -- Paint features onto grid
    -- PERF: Can slightly optimise this loop (which could be important since it gets called a lot)
    -- by deciding which function we're going to use first rather than have this unwieldy if statement
    if shape_type == "quad" or shape_type == "ellipse" or shape_type == "trapese" or item.type == "proc" then
      -- 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 item.type == "proc" or shape_type == "quad"
            or (shape_type == "ellipse" and hyper.paint.inside_oval(x,y,item))
            or (shape_type == "trapese" and hyper.paint.inside_trapese(x,y,item))
            or (type(shape_type) == "function" and hyper.paint.inside_custom(x,y,item)) then

            -- For procedural painting we get the feature cell-by-cell
            if item.type == "proc" then
              local mx,my = hyper.paint.map_to_unit(x,y,item)
              -- TODO: Allow proc paint to return an additional parameter containing other properties e.g. tile, monsters, clouds, items, stuff...
              --       (Although often we just want to return generic floor shapes and then tile and populate them later)
              feature_type,feature_usage = item.callback(x,y,mx,my)
              if feature_type ~= nil then
                feature,space,solid,wall = hyper.paint.feature_flags(feature_type,options)
              else
                feature = nil
              end
            end

            -- Work out where to actually paint
            if feature ~= nil then
              local ax,ay = x,y
              if item.wrap then
                ax = x % usage_grid.width
                ay = y % usage_grid.height
              end
              local usage = { solid = solid, feature = feature, space = space, open = open, wall = wall }
              if feature_usage ~= nil then
                hyper.merge_options(usage,feature_usage)
              end
              hyper.usage.set_usage(usage_grid,ax,ay, usage)
            end
          end
        end
      end
    elseif shape_type == "plot" then
      if item.points ~= nil then
        for i,pos in ipairs(item.points) do
          hyper.usage.set_usage(layout_grid, pos.x, pos.y,{ solid = solid, feature = feature, space = space, open = open, wall = wall })
        end
      else
        hyper.usage.set_usage(layout_grid, item.x, item.y, { solid = solid, feature = feature, space = space, open = open, wall = wall })
      end
    end
  end

end

function hyper.paint.feature_flags(feature_type,options)
  local feature
  if feature_type == "floor" then
    feature = options.layout_floor_type
  elseif feature_type == "wall" then
    feature = options.layout_wall_type or "rock_wall"
  elseif feature_type == "space" then
    feature = "space"
  end

  local space = (feature == "space")
  local solid = not space and not (feat.has_solid_floor(feature) or feat.is_door(feature))
  local wall = feat.is_wall(feature)
  return feature,space,solid,wall
end

-- Maps draw positions onto a unit square <0,0> to <1,1>
-- TODO: Something is going wrong with this algorithm because a 4x4 circle shouldn't look
--       like a square, but I don't see anything wrong with the math.
function hyper.paint.map_to_unit(x,y,item)

  local sx,sy = item.corner2.x - item.corner1.x,item.corner2.y - item.corner1.y

  -- Slightly scale down the input grid to add 0.5 squares padding on all sides. This should solve accuracy errors
  -- we otherwise get at the edges of shapes from attempting to draw a continuous geometric shape onto a discrete grid.
  local rx,ry = (x - item.corner1.x) * (sx-1)/sx + 0.5,(y - item.corner1.y) * (sy-1)/sy + 0.5

  -- Finally map onto the unit square
  return rx/sx,ry/sy
end

-- Determine if a point is inside an oval
-- TODO: http://en.wikipedia.org/wiki/Midpoint_circle_algorithm
--       http://en.wikipedia.org/wiki/Midpoint_circle_algorithm
--       Using a variant of that algorithm and adapt to ovals might give more reliable results and be quicker
function hyper.paint.inside_oval(x,y,item)
  -- Circle test
  local ax,ay = hyper.paint.map_to_unit(x,y,item)
  return (math.pow(ax * 2 - 1,2) + math.pow(ay * 2 - 1,2)) <= 1
end

-- Determine if a point is inside a simple trapeze
-- TODO: Handle rotation and allow offsetting the top and bottom on the x axis for more complex shapes
function hyper.paint.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

  local ax,ay = hyper.paint.map_to_unit(x,y,item)
  local expected_width = ay * width2 + (1-ay) * width1
  return math.abs(ax-0.5) < (expected_width / 2)

end

function hyper.paint.inside_custom(x,y,item)
  local ax,ay = hyper.paint.map_to_unit(x,y,item)
  return item.shape_type(x,y,ax,ay,item)
end

function hyper.paint.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 = hyper.usage.new_usage(gxm,gym)

  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
      local local_grid = { }
      -- 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
        local_grid[yl] = { }
        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

          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

          -- 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, { {x=-1,y=0},{x=1,y=0},{x=0,y=-1},{x=0,y=1} })
          -- 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 = { x = 0, y = 1 }, depth = 1})
            end
            -- Floor to the south
            if not local_grid[1][0].solid then
              set_usage(usage_grid,x,y, { usage = "eligible", normal = { x = 0, y = -1 }, depth = 1})
            end
            -- Floor to the west
            if not local_grid[0][-1].solid then
              set_usage(usage_grid,x,y, { usage = "eligible", normal = { x = 1, y = 0 }, depth = 1})
            end
            -- Floor to the east
            if not local_grid[0][1].solid then
              set_usage(usage_grid,x,y, { usage = "eligible", normal = { x = -1, y = 0 }, depth = 1})
            end
          else
            -- Wall all around?
            if adjacent_sum == 4 then

              local diagonal_sum = gridsum(local_grid, { {x=-1,y=-1},{x=1,y=-1},{x=-1,y=1},{x=1,y=1} })

              -- 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

function hyper.paint.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

  local usage_grid = hyper.paint.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