File: hyper_rooms.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 (410 lines) | stat: -rw-r--r-- 15,374 bytes parent folder | download | duplicates (2)
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
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
------------------------------------------------------------------------------
-- v_rooms.lua:
--
-- Functions to code-generate some common standard types of room.
------------------------------------------------------------------------------

hyper.rooms = {}

-- Picks a generator from a weighted table of generators and builds a room with it
function hyper.rooms.pick_room(build,options)

  if hyper.profile then
    profiler.push("PickRoom")
  end

  -- This callback filters out generators based on standard options which we support for all generators
  local function weight_callback(generator)
    -- max_rooms controls how many rooms _this_ generator can place before it stops
    if generator.max_rooms ~= nil and generator.placed_count ~= nil and generator.placed_count >= generator.max_rooms then
      return 0
    end
    -- min_total_rooms means this many rooms have to have been placed by _any_ generator before this one comes online
    if generator.min_total_rooms ~= nil and options.rooms_placed < generator.min_total_rooms then return 0 end
    -- max_total_rooms stops this generator after this many rooms have been placed by any generators
    if generator.max_total_rooms ~= nil and options.rooms_placed >= generator.max_total_rooms then return 0 end
    -- Generator can have a weight function instead of a flat weight
    if generator.weight_callback ~= nil then return generator.weight_callback(generator,options) end
    -- Otherwise use the generator's default weight
    return generator.weight
  end

  local weights = build.generators or options.room_type_weights

  -- Pick generator from weighted table
  local chosen = util.random_weighted_from(weight_callback,weights)
  local room

  -- Main generator loop
  local veto,tries,maxTries = false,0,50
  while tries < maxTries and (room == nil or veto) do
    tries = tries + 1
    veto = false

    room = hyper.rooms.make_room(options,chosen)

    -- Allow veto callback to throw this room out (e.g. on size, exits)
    -- TODO: This veto is still unused and I don't have a use case, maybe should delete it.
    if options.veto_room_callback ~= nil then
      veto = options.veto_room_callback(room)
    end

  end

  if hyper.profile then
    profiler.pop({ tries = tries })
  end

  return room
end

-- Makes a room object from a generator table. Optionally perform analysis
-- (default true) to determine open and connectable squares.
function hyper.rooms.make_room(options,generator)

  if hyper.profile then
    profiler.push("MakeRoom")
  end

  -- Code rooms
  if generator.generator == "code" then
    room = hyper.rooms.make_code_room(generator,options)

  -- Pick vault map by tag
  elseif generator.generator == "tagged" then
    room = hyper.rooms.make_tagged_room(generator,options)
  end

  if hyper.profile then
    profiler.pop()
    profiler.push("AnalyseRoom")
  end

  if room == nil then return nil end

  -- Do we analyse this room? Usually necessary but we might not need
  -- it e.g. random placement layouts where it doesn't matter so much
  local analyse = true
  if generator.analyse ~= nil then analyse = generator.analyse end

  if analyse then
    hyper.rooms.analyse_room(room,options)
  end

  if hyper.profile then
    profiler.pop()
    profiler.push("TransformRoom")
  end

  -- Optionally transform the room. This is used to add walls for V
  -- and other layouts but a transform doesn't have to be provided.
  local transform = generator.room_transform or options.room_transform
  if transform ~= nil then
    room = transform(room,options)
    if perform_analysis then
      hyper.rooms.analyse_room(room,options)
    end
  end

  if hyper.profile then
    profiler.pop()
    profiler.push("AnalyseRoomInternal")
  end

  if hyper.profile then
    profiler.pop()
  end

  if hyper.debug then
    dump_usage_grid_v3(room.grid)
  end

  -- Assign the room an id. This is useful later on when we carve doors etc. so we can identify which walls belong to which rooms.
  -- TODO: The id could instead be incremented for each attempt at placing a room. We can hold onto rooms that failed to place and try
  --       them again later on in case space has opened up. Not sure if this is a good idea but it could save some cycles since
  --       generating rooms is one of the more expensive operations now. Really before making any assumptions like that I should run
  --       some proper analysis of the expensiveness of different operations and get a good picture of what needs to be optimised.
  --       Perhaps it's still possible to find some ways to move expensive parts out to C++ (e.g. mask and normal generation, placement itself)
  --       whilst retaining callbacks.
  room.id = options.rooms_placed

  return room
end

function hyper.rooms.make_code_room(chosen,options)
  -- Pick a size for the room
  local size
  if chosen.size ~= nil then
    if type(chosen.size) == "function" then
      -- Callback function
      size = chosen.size(options,chosen)
    else
      -- Static size
      size = chosen.size
    end
  else
    -- Default size function
    local size_func = chosen.size_callback or hyper.rooms.size_default
    size = size_func(chosen,options)
  end

  room = {
    type = "grid",
    size = size,
    generator_used = chosen,
    grid = hyper.usage.new_usage(size.x,size.y)
  }

  -- Paint and decorate the layout grid
  hyper.paint.paint_grid(chosen.paint_callback(room,options,chosen),options,room.grid)
  if chosen.decorate_callback ~= nil then chosen.decorate_callback(room.grid,room,options) end  -- Post-production

  return room
end

-- Pick a map by tag and make a room grid from it
function hyper.rooms.make_tagged_room(chosen,options)
  -- Resolve a map with the specified tag
  local mapdef = dgn.map_by_tag(chosen.tag,true)
  if mapdef == nil then return nil end  -- Shouldn't happen when thing are working but just in case

  -- Temporarily prevent map getting mirrored / rotated during resolution because it'll seriously
  -- screw with our attempts to understand and position the vault later; and hardwire transparency because lack of it can fail a whole layout
  -- TODO: Store these tags on the room first so we can actually support them down the line ...
  dgn.tags(mapdef, "no_vmirror no_hmirror no_rotate transparent");
  -- Resolve the map so we can find its width / height
  local map, vplace = dgn.resolve_map(mapdef,false)
  local room,room_width,room_height

    -- If we can't find a map then we're probably not going to find one
  if map == nil then return nil end
  -- Allow map to be flipped and rotated again, otherwise we'll struggle later when we want to rotate it into the correct orientation
  dgn.tags_remove(map, "no_vmirror no_hmirror no_rotate")

  local room_width,room_height = dgn.mapsize(map)
  local veto = false
  -- Check min/max room sizes are observed
  if (chosen.min_size == nil or not (room_width < chosen.min_size or room_height < chosen.min_size))
    and (chosen.max_size == nil or not (room_width > chosen.max_size or room_height > chosen.max_size)) then

    room = {
      type = "vault",
      size = { x = room_width, y = room_height },
      map = map,
      vplace = vplace,
      generator_used = chosen,
      grid = hyper.usage.new_usage(room_width,room_height)
    }

    room.preserve_wall = dgn.has_tag(room.map, "no_wall_fixup")
    room.no_windows = dgn.has_tag(room.map, "no_windows")

    -- Check all four directions for orient tag before we create the wals data, since the existence of a
    -- single orient tag makes the other walls ineligible
    room.tags = {}
    for n = 0, 3, 1 do
      if dgn.has_tag(room.map,"vaults_orient_" .. vector.normals[n+1].name) then
        room.has_orient = true
        room.tags[n] = true
      end
    end
    -- Make usage grid by feature inspection; will be used to compute wall data
    for m = 0, room.size.y - 1, 1 do
      for n = 0, room.size.x - 1, 1 do
        local inspected = { }
        inspected.feature, inspected.exit, inspected.space = dgn.inspect_map(vplace,n,m)
        inspected.solid = not (feat.has_solid_floor(feature) or feat.is_door(feature))
        inspected.feature = dgn.feature_name(inspected.feature)
        hyper.usage.set_usage(room.grid,n,m,inspected)
      end
    end
    found = true
  end
  return room
end

-- Analyse the exit squares of a room to determine connectability / eligibility for placement.
function hyper.rooms.analyse_room(room,options)
  -- Initialise walls table
  room.walls = { }
  for n = 0, 3, 1 do
    room.walls[n] = { eligible = false }
  end

  local inspect_cells = {}
  local has_exits = false

  if hyper.profile then
    profiler.push("FirstPass")
  end

  for m = 0, room.size.y-1, 1 do
    for n = 0, room.size.x-1, 1 do
      local cell = hyper.usage.get_usage(room.grid,n,m)

      -- If we find an exit then flag this, causing non-exit edge cells to get ignored
      if cell.exit then has_exits = true end
      cell.anchors = {}
      -- Remember if the cell is a relevant connector cell
      if not cell.space and (cell.carvable or not cell.solid) then
        table.insert(inspect_cells,{ cell = cell, pos = { x = n, y = m } } )
      end
    end
  end

  if hyper.profile then
    profiler.pop()
    profiler.push("SecondPass")
  end

  -- Loop through the cells again now we know has_exits, and store all connected cells in our list of walls for later.
  for i,inspect in ipairs(inspect_cells) do
    local cell = inspect.cell
    local n,m = inspect.pos.x,inspect.pos.y

    if not has_exits or cell.exit then
      -- Analyse squares around it
      for i,normal in ipairs(room.allow_diagonals and vector.directions or vector.normals) do
        local near_pos = { x = n + normal.x,y = m + normal.y }
        local near_cell = hyper.usage.get_usage(room.grid,near_pos.x,near_pos.y)
        if near_cell == nil or near_cell.space then
          -- This is a possible anchor
          local anchor = true
          local target = inspect.pos
          local anchor_pos = normal
          -- If it's a wall we need space on the opposite side
          if cell.solid then
            anchor = false
            if cell.carvable then
              local target_cell = hyper.usage.get_usage(room.grid,normal.x - n,normal.y - m)
              if target_cell ~= nil and not target_cell.solid then
                anchor_pos = { x = 0, y = 0 }
                anchor = true
              end
            end
          end
          if anchor then
            -- Record this is as a possible anchor
            table.insert(cell.anchors, { normal = normal, pos = anchor_pos, origin = inspect.pos })
            -- table.insert(room.walls[normal.dir],{ cell = cell, pos = inspect.pos, normal = normal } )
            -- room.walls[normal.dir].eligible = true
            cell.connected = true
         end
        end
      end
      hyper.usage.set_usage(room.grid,n,m,cell)
    end
  end

  if hyper.profile then
    profiler.pop()
  end
end

-- Transforms a room by returning a new room sized 2 bigger with walls added next to all open edge squares
function hyper.rooms.add_walls(room, options)

  local walled_room = {
    type = "transform",
    size = { x = room.size.x + 2, y = room.size.y + 2 },
    generator_used = room.generator_used,
    transform = "add_walls",
    flags = room.flags,
    inner_room = room,
    inner_room_pos = { x = 1, y = 1 }
  }

  walled_room.grid = hyper.usage.new_usage(walled_room.size.x,walled_room.size.y)
  -- Loop through all the squares and add the walls
  for m = 0, walled_room.size.y-1, 1 do
    for n = 0, walled_room.size.x-1, 1 do
      -- Get corresponding square from original room
      local usage = hyper.usage.get_usage(room.grid,n-1,m-1)
      if usage == nil then usage = { space = true } end
      -- If it's not space, copy usage from the inner room
      if not usage.space then
        usage.inner = true
        hyper.usage.set_usage(walled_room.grid,n,m,usage)
      else
        -- Otherwise check if we're bordering any open squares and therefore need to draw a wall.
        local any_open = false
        for i,normal in ipairs(vector.directions) do
          local near_cell = hyper.usage.get_usage(room.grid,n+normal.x-1,m+normal.y-1)
          if near_cell == nil then near_cell = { space = true } end
          if not near_cell.space and not near_cell.solid then
            any_open = true
            break
          end
        end
        if any_open then
          -- There was at least one open square so we need to make a wall which *could* be carvable.
          -- Note: carvable doesn't mean this can necessarily be used as a door, e.g. Fort crennelations ... that will still happen in room analysis. It
          -- makes the logic overall much simpler.
          -- TODO: Allow diagonal doors here too. Diagonals are problematic (in the previous loop and in analyse_room) because we'd get overlapping anchors
          --       with adjacent walls, this really doesn't sound good.
          local wall_usage = { feature = "rock_wall", wall = true, protect = true }
          for i,normal in ipairs(vector.normals) do
            local near = hyper.usage.get_usage(room.grid,n+normal.x-1,m+normal.y-1)
            if near ~= nil and near.connected then
              wall_usage.carvable = true
              wall_usage.connected = true  -- TODO: Might be redundant
            end
          end
          hyper.usage.set_usage(walled_room.grid, n, m, wall_usage)
        end
      end
    end
  end

  return walled_room
end

function hyper.rooms.add_buffer(room, options)

  local walled_room = {
    type = "transform",
    size = { x = room.size.x + 2, y = room.size.y + 2 },
    generator_used = room.generator_used,
    transform = "add_buffer",
    flags = room.flags,
  }

  -- Take on the existing inner room if available (wall+buffer)
  walled_room.inner_room = room.inner_room or room
  walled_room.inner_room_pos = room.inner_room_pos and { x = room.inner_room_pos.x+1, y = room.inner_room_pos.y+1 } or { x = 1, y = 1 }
  walled_room.grid = hyper.usage.new_usage(walled_room.size.x,walled_room.size.y)

  -- Loop through all the squares and add the buffer
  for m = 0, walled_room.size.y-1, 1 do
    for n = 0, walled_room.size.x-1, 1 do
      -- Get corresponding square from original room
      local usage = hyper.usage.get_usage(room.grid,n-1,m-1)
      if usage == nil then usage = { space = true } end
      -- If it's not space, copy usage from the inner room
      if not usage.space then
        usage.inner = true
        hyper.usage.set_usage(walled_room.grid,n,m,usage)
      else
        -- Otherwise check if we're bordering any non-space and therefore need to create a buffer
        local any_open = false
        for i,normal in ipairs(vector.directions) do
          local near_cell = hyper.usage.get_usage(room.grid,n+normal.x-1,m+normal.y-1)
          if near_cell == nil then near_cell = { space = true } end
          if not near_cell.space then
            any_open = true
            break
          end
        end
        if any_open then
          hyper.usage.set_usage(walled_room.grid, n, m, { space = true, buffer = true })
        end
      end
    end
  end

  return walled_room
end

function hyper.rooms.add_buffer_walls(room, options)
  return hyper.rooms.add_buffer(hyper.rooms.add_walls(room,options),options)
end