File: v_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 (803 lines) | stat: -rw-r--r-- 36,928 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
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
------------------------------------------------------------------------------
-- v_rooms.lua:
--
-- This file handles the main task of selecting rooms and drawing them onto
-- the layout.
------------------------------------------------------------------------------

hypervaults.rooms = {}

-- Moves from a start point along a move vector mapped to actual vectors xVector/yVector
-- This allows us to deal with coords independently of how they are rotated to the dungeon grid
local function vaults_vector_add(start, move, xVector, yVector)

  return {
    x = start.x + (move.x * xVector.x) + (move.y * yVector.x),
    y = start.y + (move.x * xVector.y) + (move.y * yVector.y)
  }

end

-- Rotates count * 90 degrees anticlockwise
-- Highly rudimentary, and not actually used for anything, keeping around for now just in case.
local function vector_rotate(vec, count)
  if count > 0 then
    local rotated = { x = -vec.y, y = vec.x }
    count = count - 1
    if count <= 0 then return rotated end
    return vector_rotate(rotated,count)
  end
  if count < 0 then
    local rotated = { x = vec.y, y = -vec.x }
    count = count + 1
    if count >= 0 then return rotated end
    return vector_rotate(rotated,count)
  end
  return vec
end

    -- TODO: Following logic to be reintegrated into code room generation; meaning large empty rooms could have other rooms placed inside them again
  -- local set_empty = crawl.coinflip() -- Allow the room to be used for more rooms or not?/
   --   elseif x > origin.x + 1 and x < opposite.x-1 and y > origin.y - 1 and y < opposite.y - 1 then  -- Leaving a 2 tile border
    --    vaults_set_usage(usage_grid,x,y,{usage = "open"})
    --  else
    --    vaults_set_usage(usage_grid,x,y,{usage = "restricted", reason = "border"})
    --  end

local function make_code_room(options,chosen)
  -- Pick a size for the room
  local size
  if chosen.pick_size_callback ~= nil then
    size = chosen.pick_size_callback(options)
  else
    local min_size, max_size = options.min_room_size,options.max_room_size
    if chosen.min_size ~= nil then min_size = chosen.min_size end
    if chosen.max_size ~= nil then max_size = chosen.max_size end
    local diff = max_size - min_size + 1
    size = { x = min_size + crawl.random2(crawl.random2(diff)), y = min_size + crawl.random2(crawl.random2(diff)) }
  end

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

  -- Make grid empty space
  for n = 0, size.x - 1, 1 do
    for m = 0, size.y - 1, 1 do
      vaults_set_layout(room.grid,n,m, { solid = true, space = true, feature = "space" })
    end
  end

  -- Paint and decorate the layout grid
  paint_grid(chosen.paint_callback(room,options,chosen),options,room.grid)
  if _VAULTS_DEBUG then dump_layout_grid(room.grid) end
  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
local function make_tagged_room(options,chosen)
  -- 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 ...
  local old_tags = dgn.tags(mapdef)
  dgn.tags(mapdef, "no_vmirror no_hmirror no_rotate passable")
  -- 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
    dgn.tags(mapdef, nil)
    dgn.tags(mapdef, old_tags)
    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")
  -- restore the original tags to mapdef, since this state is persistent
  dgn.tags(mapdef, nil)
  -- TODO: if a vault is not tagged passable, this addition is permanent; does
  -- it affect anything?
  dgn.tags(mapdef, old_tags .. " passable")

  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 = {}
    }

    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_" .. hypervaults.normals[n+1].name) then
        room.has_orient = true
        room.tags[n] = true
      end
    end
    -- Make grid by feature inspection; will be used to compute wall data
    for m = 0, room.size.y - 1, 1 do
      room.grid[m] = {}
      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(inspected.feature)
        inspected.feature = dgn.feature_name(inspected.feature)
        room.grid[m][n] = inspected
      end
    end
    found = true
  end
  return room
end

local function pick_room(e, options)

  -- Filters out generators that have reached their max
  local function weight_callback(generator)
    if generator.max_rooms ~= nil
        and generator.placed_count ~= nil
        and generator.placed_count >= generator.max_rooms then
      return 0
    end
    return generator.weight
  end

  local chosen
  -- Roll the chance to pick a ghost vault room if we haven't done so.
  if not options.did_necropolis_chance then
    if crawl.x_chance_in_y(dgn.necropolis_chance_percent, 100) then
      -- Find the ghost vault generator, if this somehow doesn't exist, we will
      -- fall back to the usual set of generators.
      for i, r in ipairs(options.room_type_weights) do
        if r.generator == "tagged" and r.tag == "vaults_necropolis" then
          chosen = r
        end
      end
    end
    options.did_necropolis_chance = true
  end
  -- Roll the chance to pick a Wizlab vault room if we haven't done so.
  if not options.did_wizlab_chance then
    if crawl.x_chance_in_y(dgn.wizlab_chance_percent, 100) then
      for i, r in ipairs(options.room_type_weights) do
        if r.generator == "tagged" and r.tag == "vaults_wizlab" then
          chosen = r
        end
      end
    end
    options.did_wizlab_chance = true
  end
  -- Roll the chance to pick a Desolation vault room if we haven't done so.
  if not options.did_desolation_chance then
    if crawl.x_chance_in_y(dgn.desolation_chance_percent, 100) then
      for i, r in ipairs(options.room_type_weights) do
        if r.generator == "tagged" and r.tag == "vaults_desolation" then
          chosen = r
        end
      end
    end
    options.did_desolation_chance = true
  end

  -- We aren't choosing a ghost vault or a vault for wizlab / desolation /
  -- necropolis portals, so pick a generator from the weighted table.
  if chosen == nil then
    chosen = util.random_weighted_from(weight_callback,
                                       options.room_type_weights)
  end

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

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

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

    if room ~= nil and room.grid ~= nil then

      -- Create a grid for the wall mask. It's 1 unit bigger on all sides than the standard grid. In this we will track where we need to place walls and also
      -- if those walls are connected, i.e. border onto a passable square in such a way that they can be used as a door.
      room.walls = { }
      room.mask_grid = { }
      local has_exits = room.has_orient and true or false
      for m = -1, room.size.y, 1 do
        room.mask_grid[m] = {}
        for n = -1, room.size.x, 1 do
          local cell = vaults_get_layout(room.grid,n,m)

          if cell.exit then has_exits = true end

          if not cell.space then
            --if not cell.solid then -- TODO: Check for wall (this is NOT the same as 'solid')
              room.mask_grid[m][n] = { vault = true, wall = false, space = false }
            --else
            -- room.mask_grid[m][n] = { vault = true, wall = true, space = false }

          else
            -- Analyse layout squares around it
            -- TODO: Existing wall cells in the vault can be considered space for this purpose, it'll stop us getting a double layer of wall around some vaults
            local all_space = true
            for ax = n-1, n+1, 1 do
              for ay = m-1, m+1, 1 do
                if ax ~= n or ay ~= m then
                  local near_cell = vaults_get_layout(room.grid,ax,ay)
                  if not near_cell.space then all_space = false end
                end
              end
            end
            if all_space then
              room.mask_grid[m][n] = { space = true, wall = false, vault = false }
            else
              local adj_cells = {
                vaults_get_layout(room.grid,n,m-1),
                vaults_get_layout(room.grid,n-1,m),
                vaults_get_layout(room.grid,n,m+1),
                vaults_get_layout(room.grid,n+1,m)
              }
              local adj_space,adj_exit,adj_exit_cell = 0,0,nil
              for i,adj_cell in ipairs(adj_cells) do
                if adj_cell.space then
                  adj_space = adj_space + 1
                else
                  if not adj_cell.solid then
                    adj_exit = i
                    adj_exit_cell = adj_cell
                  end
                end
              end
              local wall_mask
              if adj_space == 3 and adj_exit > 0 then
                wall_mask = { wall = true, connected = true, dir = (adj_exit + 1) % 4, cell = cell, exit_cell = adj_exit_cell }
              else
                wall_mask = { wall = true }
              end
              room.mask_grid[m][n] = wall_mask
            end
          end
        end
      end

      -- Loop through the cells again now we know has_exits, and store all connected cells in our list of walls for later.
      for m = -1, room.size.y, 1 do
        for n = -1, room.size.x, 1 do
          local wall_mask = room.mask_grid[m][n]
          if wall_mask.connected then
            local cell = wall_mask.exit_cell
            local is_open = true
            if cell.space or (has_exits and not cell.exit and not (room.has_orient and room.tags[wall_mask.dir])) then
              is_open = false
            end
            wall_mask.cell.open = is_open
            if not is_open then wall_mask.connected = false
            else
              -- Remember in a list of wall cells for connecting
              local wall_cell = { feature = cell.feature, exit = cell.exit, exit_cell = cell, cell = wall_mask.cell, open = is_open, space = cell.space, pos = { x = n, y = m }, dir = wall_mask.dir }
              if room.walls[wall_cell.dir] == nil then room.walls[wall_cell.dir] = {} end
              table.insert(room.walls[wall_cell.dir], wall_cell)
            end
          end
        end
      end
      for n = 0, 3, 1 do
        if room.walls[n] == nil then
          room.walls[n] = { eligible = false }
        else
          room.walls[n].eligible = true
        end
      end

      -- Allow veto callback to throw this room out (e.g. on size, exits)
      if options.veto_room_callback ~= nil then
        veto = options.veto_room_callback(room)
      end
    end
  end

  if _VAULTS_DEBUG then
    dump_mask_grid(room)
  end

  return room

end

-- TODO: This callback only actually applies to V, right now at least. Should specify it in the config instead of always calling.
local function analyse_vault_post_placement(usage_grid,room,result,options)
  local perform_subst = true
  if room.preserve_wall or room.wall_type == nil then perform_subst = false end
  result.stairs = { }
  for i,coord in ipairs(result.coords_list) do
    p = coord.grid_pos
    if dgn.in_bounds(p.x,p.y) and
      (feat.is_stone_stair(p.x,p.y)) or
      -- On V:1 the branch entrant stairs don't count as stone_stair; we need to check specifically for the V exit stairs
      -- to avoid overwriting the Crypt entrance stairs!
      dgn.feature_name(dgn.grid(p.x,p.y)) == "exit_vaults" then
      -- Remove the stair and remember it
      dgn.grid(p.x,p.y,"floor") --TODO: Be more intelligent about how to replace it
      table.insert(result.stairs, { pos = { x = p.x, y = p.y } })
    end
    -- Substitute rock walls for surrounding wall type
    if perform_subst and dgn.feature_name(dgn.grid(p.x,p.y)) == "rock_wall" then
      dgn.grid(p.x,p.y,room.wall_type)
    end
  end
end

function place_vaults_rooms(e, data, room_count, options)

  if options == nil then options = vaults_default_options() end
  local results, rooms_placed, times_failed, total_failed = { }, 0, 0, 0

  -- Keep trying to place rooms until we've had max_room_tries fail in a row or we reach the max
  while rooms_placed < room_count and times_failed < options.max_room_tries do
    local placed = false

    -- Pick a room
    local room = pick_room(e, options)
    -- Attempt to place it. The placement function will try several times to find somewhere to fit it.
    if room ~= nil then
      local result = place_vaults_room(e,data,room,options)
      if result ~= nil and result.placed then
        placed = true
        rooms_placed = rooms_placed + 1  -- Increment # rooms placed
        times_failed = 0 -- Reset fail count
        -- Perform analysis for stairs (and perform inner wall substitution if applicable)
        analyse_vault_post_placement(data,room,result,options)
        table.insert(results,result)
        -- Increment the count of rooms of this type
        if room.generator_used ~= nil then
          if room.generator_used.placed_count == nil then room.generator_used.placed_count = 0 end
          room.generator_used.placed_count = room.generator_used.placed_count + 1
        end
      end

    end

    if not placed then
      times_failed = times_failed + 1 -- Increment failure count
      total_failed = total_failed + 1
    end

  end

  -- Now we need some stairs
  local stairs = { }
  for i, r in ipairs(results) do
    for j, s in ipairs(r.stairs) do
      if s ~= nil then table.insert(stairs,s) end
    end
  end

  -- Place three up, three down, plus two of each hatch
  local stair_types = {
    "stone_stairs_down_i", "stone_stairs_down_ii",
    "stone_stairs_down_iii", "stone_stairs_up_i",
    "stone_stairs_up_ii", "stone_stairs_up_iii" }

    -- We could place some hatches in rooms but from discussion on IRC just let them place naturally -
    -- they inherently carry some risk and can't be used for pulling tactics so it's fine to have them in corridors.
    -- "escape_hatch_up", "escape_hatch_down",
    -- "escape_hatch_up", "escape_hatch_down" }

  for n,stair_type in ipairs(stair_types) do
    -- Do we have any left?
    if #stairs == 0 then break end
    -- Any random stair
    local i = crawl.random_range(1, #stairs)
    local stair = stairs[i]
    -- Place it
    dgn.grid(stair.pos.x, stair.pos.y, stair_type)
    -- Remove from list
    table.remove(stairs,i)
  end

  -- Set MMT_VAULT across the whole map depending on usage. This way the dungeon builder knows where to place standard vaults without messing up the layout.
  local gxm, gym = dgn.max_bounds()
  for p in iter.rect_iterator(dgn.point(0, 0), dgn.point(gxm - 1, gym - 1)) do
    local usage = vaults_get_usage(data,p.x,p.y)
    if usage ~= nil and usage.usage == "restricted" then -- or usage.usage == "eligible_open" or (usage.usage == "eligible" and usage.depth > 1) then
      dgn.set_map_mask(p.x,p.y)
    end
  end

  -- Useful to see what's going on when things are getting veto'd:
  if _VAULTS_DEBUG then dump_usage_grid(data) end

  return true
end

function place_vaults_room(e,usage_grid,room, options)

  local gxm, gym = dgn.max_bounds()

  local tries = 0
  local done = false

  local available_spots = #(usage_grid.eligibles)
  if available_spots == 0 then return { placed = false } end
  local list

  -- Have a few goes at placing; we'll select a random eligible spot each time and try to place the vault there
  while tries < options.max_place_tries and not done do
    tries = tries + 1
    -- Select an eligible spot from the list in usage_grid
    local usage = usage_grid.eligibles[crawl.random_range(1,available_spots)]
    local spot = usage.spot
    -- Scan a 5x5 grid around an open spot, see if we find an eligible_open wall to attach to. This makes us much more likely to
    -- join up rooms in open areas, and reduces the amount of 1-tile-width chokepoints between rooms. It's still fairly simplistic
    -- though and maybe we could do this around the whole room area later on instead?
    if usage.usage == "open" then
      local near_eligibles = {}
      for p in iter.rect_iterator(dgn.point(spot.x-2,spot.y-2),dgn.point(spot.x+2,spot.y+2)) do
        local near_usage = vaults_get_usage(usage_grid,p.x,p.y)
        if near_usage ~= nil and near_usage.usage == "eligible_open" then
          table.insert(near_eligibles, { spot = p, usage = near_usage })
        end
      end
      -- Randomly pick one of the new spots; maybe_place_vault will at least attempt to attach the room here instead, if possible
      if #near_eligibles > 0 then
        local picked = near_eligibles[crawl.random2(#near_eligibles)+1]
        spot = picked.spot
        usage = picked.usage
      end
    end

    -- Attempt to place the room on the wall at a random position
    list = vaults_maybe_place_vault(e, spot, usage_grid, usage, room, options)
    if list ~= nil and list ~= false then
      done = true
    end
  end
  return { placed = done, coords_list = list }
end

function hypervaults.rooms.decorate_walls_open(state, connections, door_required, has_windows)

  for i, door in ipairs(connections) do
    dgn.grid(door.grid_pos.x,door.grid_pos.y, "floor")
  end

end

function hypervaults.rooms.decorate_walls(state, connections, door_required, has_windows)

  -- TODO: Use decorator callbacks for these so we can vary by layout
  local have_door = true
  if not door_required then
    have_door = crawl.x_chance_in_y(4,5)
  end

  if have_door then
    -- Usually we place one door on a wall
    local num_doors = 1
    -- Sometimes have a chance to place more
    if crawl.one_chance_in(3) then
      -- Although this still might pick only 1
      num_doors = math.abs(crawl.random2avg(9,4)-4)+1  -- Should tend towards 1 but rarely can be up to 5
    end
    for n=1,num_doors,1 do
      -- This could pick a door we've already picked; doesn't matter, this just makes more doors even slightly
      -- less likely
      local door = connections[crawl.random2(#connections)+1]
      if door ~= nil then
        dgn.grid(door.grid_pos.x,door.grid_pos.y, "closed_door")
        door.is_door = true
      end
    end
  end

  -- Optionally place windows
  if has_windows and not state.room.no_windows and crawl.one_chance_in(14) then
    local num_windows = math.abs(crawl.random2avg(5,3)-2)+2  -- Should tend towards 2 but rarely can be up to 4

    for n=1,num_windows,1 do
      -- This could pick a door we've already picked; doesn't matter, this just makes more doors even slightly less likelier
      local door = connections[crawl.random2(#connections)+1]
      if door ~= nil and not door.is_door then
        dgn.grid(door.grid_pos.x,door.grid_pos.y, "clear_stone_wall")
        door.is_window = true
      end
    end
  end
end

-- Attempts to place a room by picking a random available spot and checking it'll fit.
-- GENERAL TODO:
-- 1. This function is massive and could do with breaking up into a few parts (calculate normals; check space / generate coords; paint and set usage)
-- 2. Open room placement is slow. When we place an open room we should set ineligible squares all around it (at e.g. 2 tiles distance). This will reduce
--    attempts to place overlapping rooms and reduce the overall number of 'open' spots, meaning we get increasingly more likely to hit an 'eligible_open' instead.
--    These new restricted zones still need to reference the room so they don't stop proper attachment of new rooms.
-- 3. Funny-shaped rooms can still produce VETO-causing bubbles even with all this checking, this can typically happen when two funny-shaped rooms connect
--    via two separate walls, creating an enclosed spot in between. Thankfully this doesn't happen very often (yet). There are two ways to avoid this: a) Relax
--    the VETO restriction, this shouldn't actually be a problem because Crawl automatically generates trapdoors anyway! Crawl could instead / additionally fill the area with
--    no_tele_into (but on its own this doesn't solve the problem that an off-level hatch/shaft could land the player there, and possibly end them up in an a multi-level
--    hatch loop!)  b) Actually detect and handle such enclosed areas during layout generation. This sounds hard to do in a performance-friendly way but it only needs
--    a check when we're attaching to an open_eligible, need to check each tile that's still open for connectivity to the room we just placed (or some sort of flood fill).
-- 4. General performance - it's definitely ok in V but some of the experimental layouts I've tried have more problems. A big factor is needing to eliminate more eligible
--    spots when we place rooms because once a level is really busy most placement attempts are doomed. When we place a room we should send out rays in the directions
--    of normals and if we hit something close (or map edge) then that wall is no longer eligible. The trigger proximity needs to be 1 less than the smallest room that can fit.
--    This should vastly help.
-- 5. Additional corridor / door carving. Send rays out from walls (in combination with the above) and if a room is within a suitable proximity (e.g. < 5) then we can
--    carve out a corridor and doors.
-- 6. Room flipping. Should be fairly straightforward now; we need to a) invert the x or y normal dependending on which way we're flipping, b) appropriately adjust the logic
--    of vault placement and c) similarly flip the final usage normals. I'm just hesitant to throw this spanner in the works before everything else is 100% working.
function vaults_maybe_place_vault(e, pos, usage_grid, usage, room, options)
  local v_normal, v_wall, v_normal_dir,wall

  -- Choose which wall of the room to attach to this spot (walls were determined in pick_room)
  local orients = { }
  for n = 0, 3, 1 do
    if room.walls[n].eligible then
      table.insert(orients,{ dir = n, wall = room.walls[n] })
    end
  end
  local count = #orients
  if count == 0 then return false end

  local chosen = orients[crawl.random2(count) + 1]
  wall = chosen.wall

  -- Pick a random open cell on the wall. This is where we'll attach the room to the door.
  if #wall == 0 then return false end -- Shouldn't happen, eligible should mean cells available
  local chosen_wall = wall[crawl.random2(#wall)+1]

  -- Get wall's normal and calculate the door vector (i.e. attaching wall)
  -- In open space we can pick a random normal for the door
  if usage.usage == "open" then
    -- Pick a random rotation for the door wall
    v_normal = hypervaults.normals[crawl.random_range(1,4)]
    v_normal_dir = v_normal.dir
  end

  -- If placing a room in an existing wall we have the normal stored already in the usage data
  if usage.usage == "eligible" or usage.usage == "eligible_open" then
    v_normal = usage.normal
    v_normal_dir = usage.normal.dir
    -- TODO: make sure usage.normal.dir is never nil and remove this loop
    if v_normal_dir == nil then
      for i,n in ipairs(hypervaults.normals) do
        if n.x == v_normal.x and n.y == v_normal.y then
          v_normal_dir = n.dir
        end
      end
    end
  end

  -- Figure out the mapped x and y vectors of the room relative to its orient
  local room_final_x_dir = (v_normal_dir + 1 - chosen_wall.dir) % 4
  local room_final_y_dir = (room_final_x_dir - 1) % 4
  local room_final_x_normal = hypervaults.normals[room_final_x_dir + 1]
  local room_final_y_normal = hypervaults.normals[room_final_y_dir + 1]

  -- Calculate how much the room will have to rotate to match the new orientation
  local final_orient = (room_final_x_dir + 1) % 4

  -- Now we can use those vectors along with the position of the connecting wall within the room, to find out where the (0,0) corner
  -- of the room lies on the map (and use that coord for subsequent calculations within the room grid)
  local room_base = vaults_vector_add(pos, { x = -(chosen_wall.pos.x), y = -(chosen_wall.pos.y) }, room_final_x_normal, room_final_y_normal)

  -- State object to send to callbacks
  local state = {
    room = room,
    usage = usage,
    pos = pos,
    wall = chosen_wall,
    base = room_base,
    dir = final_orient,
    usage_grid = usage_grid
  }

  -- Layout configuration can now veto this room placement with a callback
  if options.veto_place_callback ~= nil then
    local result = options.veto_place_callback(state)
    if result == true then return false end
  end

  -- Loop through all coords of the room and its walls. Map to grid coords and add all this data into a list to make iterating through
  -- map squares easier in the future
  local coords_list = {}
  local is_clear = true

  for m = -1, room.size.y, 1 do
    for n = -1, room.size.x, 1 do
      local coord = { room_pos = { x = n, y = m } }
      coord.grid_pos = vaults_vector_add(room_base, coord.room_pos, room_final_x_normal, room_final_y_normal)
      coord.grid_usage = vaults_get_usage(usage_grid, coord.grid_pos.x, coord.grid_pos.y)
      -- Out of bounds (maybe we should catch this earlier by checking both corners)
      if coord.grid_usage == nil then
        is_clear = false
        break
      end
      coord.room_cell = vaults_get_layout(room.grid,coord.room_pos.x,coord.room_pos.y)
      coord.room_mask = vaults_get_layout(room.mask_grid,coord.room_pos.x,coord.room_pos.y)
      -- Check cells of non-space or wall
      if coord.room_mask.vault or coord.room_mask.wall then
        local target_usage = coord.grid_usage
        -- Check we are allowed to build here
        -- If our room type is "open" then we need to find all open or eligible_open squares (meaning we take up
        -- open space or we are attaching to an existing building). If our room type is "eligible" then we need to find all unused or eligible squares (meaning
        -- we are building into empty walls or attaching to more buildings). "Restricted" squares only need to fail for certain reasons; quite often our walls will
        -- overwrite walls of other rooms which may be "restricted" due to the contents of the room, but restricted squares inside vaults or too near to existing walls
        -- ("border") must fail.
        if (target_usage == nil or (target_usage.usage == "restricted" and (target_usage.reason == "border" or target_usage.reason == "vault" or target_usage.reason == "door"))
          or (usage.usage == "eligible" and target_usage.usage ~= "eligible" and target_usage.usage ~= "none")
          or (usage.usage == "open" and target_usage.usage ~= "open")
          or (usage.usage == "eligible_open" and target_usage.usage ~= "open"
             and not ((target_usage.usage == "eligible_open" or (target_usage.usage == "restricted" and target_usage.reason == "wall")) and usage.room == target_usage.room)))
             then
          is_clear = false
          break
        end

        -- For open rooms, check that the only _nearby_ room (within 2 tiles) is the one we are supposed to be attaching to.
        -- This prevents us blocking off doors of rooms that just happened to be nearby, and also prevents disconnected layouts because
        -- if open rooms connect in non-tree-like fashion it produces enclosed bubbles that are extremely difficult to either detect
        -- or fix. (It would be nice to do something about this but probably too difficult to be worth it right now).
        -- As it stands this is not highly optimal and could certainly be improved.
        if (usage.usage == "open" or usage.usage == "eligible_open") and coord.room_mask.wall then
          for mN = -1, 1, 1 do
            for nN = -1, 1, 1 do
              local posN = { x = coord.room_pos.x + nN, y = coord.room_pos.y + mN }
              local cellN = vaults_get_layout(room.grid,posN.x,posN.y)
              local maskN = vaults_get_layout(room.mask_grid,posN.x,posN.y)
              if maskN == nil or not (maskN.wall or maskN.vault) then
                local gridN = vaults_vector_add(room_base, posN, room_final_x_normal, room_final_y_normal)
                local usageN = vaults_get_usage(usage_grid, gridN.x, gridN.y)
                if (usageN.usage ~= "open" and not (usageN.usage == "restricted" and usageN.reason == "border") and (usageN.room == nil or usageN.room ~= usage.room)) then -- and (usageN.usage == "vault" or usageN.usage == "eligible_open"))) then
                  is_clear = false
                end
              end
            end
          end
          if not is_clear then break end
        end

        table.insert(coords_list, coord)
      end
    end
    if not is_clear then break end
  end

  -- No clear space found, the function fails and we'll look for a new spot
  if not is_clear then return false end

  -- Lookup the two corners of the room and map to oriented coords to find the top-leftmost and bottom-rightmost coords relative to dungeon orientation.
  -- TODO: This should only be needed for vault placement and we only need the origin I think
  local c1,c2 = room_base,vaults_vector_add(room_base, { x = room.size.x - 1, y = room.size.y - 1 }, room_final_x_normal, room_final_y_normal)
  local origin = { x = math.min(c1.x,c2.x), y = math.min(c1.y,c2.y) }

  -- Store; will also be used for vault analysis (?)
  room.origin = origin

  -- Wall type. Originally this was randomly varied but it didn't work well when rooms bordered.
  room.wall_type = options.layout_wall_type

  -- Now loop through every square of the room; update the dungeon grid and wall usage table, and make a list
  -- of connectable walls - i.e. walls with open space on both side; they could be the original orientation wall,
  -- 'incidental' walls where the room just happened to overlap another room's eligible walls, or for open rooms
  -- this will be all sides. The wall lists will get passed to decorator functions for painting doors and so forth.
  -- We don't have to end up with a door on the original orientation spot, although we will ensure that at least
  -- one cell from that wall gets carved, to ensure connectivity.
  local wall_usage, new_depth = "eligible", 2
  if usage.usage == "open" or usage.usage == "eligible_open" then wall_usage = "eligible_open" end
  if usage.depth ~= nil then new_depth = usage.depth + 1 end  -- Room depth

  local incidental_connections = { }
  local door_connections = { }
  local open_connections = { }
  for i, coord in ipairs(coords_list) do

    -- Paint walls
    if coord.room_mask.wall then
      dgn.grid(coord.grid_pos.x, coord.grid_pos.y, room.wall_type)
    -- Draw grid features (for code rooms)
    elseif room.type == "grid" then
      if not coord.room_cell.space and coord.room_cell.feature ~= "space" and coord.room_cell.feature ~= nil then
        dgn.grid(coord.grid_pos.x, coord.grid_pos.y, coord.room_cell.feature)
      end
    end

    -- Handle usage
    local grid_cell = coord.room_cell
    local mask_cell = coord.room_mask

    if mask_cell.vault then
      if grid_cell.empty then
        vaults_set_usage(usage_grid,coord.grid_pos.x,coord.grid_pos.y,{ usage = "empty" })
      else
        vaults_set_usage(usage_grid,coord.grid_pos.x,coord.grid_pos.y,{ usage = "restricted", room = room, reason = "vault" })
      end

    elseif mask_cell.wall then

      -- Check if overlapping existing wall
      local grid_coord = coord.grid_pos
      local current_usage = coord.grid_usage

      if current_usage.usage == "open" and mask_cell.connected then
        -- Count all sides as door connections; potentially we'll get doors on multiple side
        table.insert(open_connections, { room_coord = coord.room_pos, grid_pos = grid_coord, usage = current_usage, mask = mask_cell })
      end
      if (current_usage.usage == "eligible" and usage.usage == "eligible") or (current_usage.usage == "eligible_open" and usage.usage == "eligible_open") then
      -- Overlapping cells with current room, these are potential door wall candidates
        if usage.room == current_usage.room then
          -- Door connections
          vaults_set_usage(usage_grid,grid_coord.x,grid_coord.y,{ usage = "restricted", room = room, reason = "door" })
          if mask_cell.connected then
            table.insert(door_connections, { room_coord = coord.room_pos, grid_pos = grid_coord, usage = current_usage, mask = mask_cell })
          end
        else
          -- Incidental connections, we can optionally make doors here
          vaults_set_usage(usage_grid,grid_coord.x,grid_coord.y,{ usage = "restricted", room = room, reason = "cell" })
          if mask_cell.connected then
            if incidental_connections[mask_cell.dir] == nil then incidental_connections[mask_cell.dir] = {} end
            table.insert(incidental_connections[mask_cell.dir], { room_coord = coord.room_pos, grid_pos = grid_coord, usage = current_usage, mask = mask_cell })
          end
        end
      end
      -- Carving into rock / open space
      if mask_cell.connected and ((current_usage.usage == "none" and usage.usage == "eligible") or (current_usage.usage == "open" and (usage.usage == "eligible_open" or usage.usage == "open"))) then
        local u_wall_dir = (mask_cell.dir + final_orient) % 4
        vaults_set_usage(usage_grid,grid_coord.x,grid_coord.y,{ usage = wall_usage, normal = hypervaults.normals[u_wall_dir + 1], room = room, depth = new_depth, cell = grid_cell, mask = mask_cell })
      elseif not mask_cell.connected and (current_usage.usage == "open" and (usage.usage == "eligible_open" or usage.usage == "open")) then
        -- Should prevent nearby rooms from thinking this space is open
        vaults_set_usage(usage_grid,grid_coord.x,grid_coord.y,{ usage = "restricted", room = room, depth = new_depth, cell = grid_cell, mask = mask_cell, reason = "wall" })
      end
    end
  end

  -- Place the vault if we have one
  if room.type == "vault" then
    -- We can only rotate a map clockwise or anticlockwise. To rotate 180 we just flip on both axes.
    if final_orient == 0 then dgn.reuse_map(room.vplace,origin.x,origin.y,false,false,0,true,true)
    elseif final_orient == 1 then dgn.reuse_map(room.vplace,origin.x,origin.y,false,false,-1,true,true)
    elseif final_orient == 2 then dgn.reuse_map(room.vplace,origin.x,origin.y,true,true,0,true,true)
    elseif final_orient == 3 then dgn.reuse_map(room.vplace,origin.x,origin.y,false,false,1,true,true) end
  end

  -- Use decorator callbacks to decorate the connector cells; e.g. doors and windows, solid wall, open wall ...
  local decorate_callback = options.decorate_walls_callback
  if decorate_callback == nil then decorate_callback = hypervaults.rooms.decorate_walls end

  if #open_connections > 0 then decorate_callback(state,open_connections,true,true) end
  if #door_connections > 0 then decorate_callback(state,door_connections,true,true) end

  -- Have a chance to add doors / windows to each other side of the room
  for n = 0, 3, 1 do
    if incidental_connections[n] ~= nil and #(incidental_connections[n]) > 0 then
      decorate_callback(state,incidental_connections[n],false,true)
    end
  end

  return coords_list
end