1------------------------------------------------------------------------------
2-- v_rooms.lua:
3--
4-- Functions to code-generate some common standard types of room.
5------------------------------------------------------------------------------
6
7hyper.rooms = {}
8
9-- Picks a generator from a weighted table of generators and builds a room with it
10function hyper.rooms.pick_room(build,options)
11
12  if hyper.profile then
13    profiler.push("PickRoom")
14  end
15
16  -- This callback filters out generators based on standard options which we support for all generators
17  local function weight_callback(generator)
18    -- max_rooms controls how many rooms _this_ generator can place before it stops
19    if generator.max_rooms ~= nil and generator.placed_count ~= nil and generator.placed_count >= generator.max_rooms then
20      return 0
21    end
22    -- min_total_rooms means this many rooms have to have been placed by _any_ generator before this one comes online
23    if generator.min_total_rooms ~= nil and options.rooms_placed < generator.min_total_rooms then return 0 end
24    -- max_total_rooms stops this generator after this many rooms have been placed by any generators
25    if generator.max_total_rooms ~= nil and options.rooms_placed >= generator.max_total_rooms then return 0 end
26    -- Generator can have a weight function instead of a flat weight
27    if generator.weight_callback ~= nil then return generator.weight_callback(generator,options) end
28    -- Otherwise use the generator's default weight
29    return generator.weight
30  end
31
32  local weights = build.generators or options.room_type_weights
33
34  -- Pick generator from weighted table
35  local chosen = util.random_weighted_from(weight_callback,weights)
36  local room
37
38  -- Main generator loop
39  local veto,tries,maxTries = false,0,50
40  while tries < maxTries and (room == nil or veto) do
41    tries = tries + 1
42    veto = false
43
44    room = hyper.rooms.make_room(options,chosen)
45
46    -- Allow veto callback to throw this room out (e.g. on size, exits)
47    -- TODO: This veto is still unused and I don't have a use case, maybe should delete it.
48    if options.veto_room_callback ~= nil then
49      veto = options.veto_room_callback(room)
50    end
51
52  end
53
54  if hyper.profile then
55    profiler.pop({ tries = tries })
56  end
57
58  return room
59end
60
61-- Makes a room object from a generator table. Optionally perform analysis
62-- (default true) to determine open and connectable squares.
63function hyper.rooms.make_room(options,generator)
64
65  if hyper.profile then
66    profiler.push("MakeRoom")
67  end
68
69  -- Code rooms
70  if generator.generator == "code" then
71    room = hyper.rooms.make_code_room(generator,options)
72
73  -- Pick vault map by tag
74  elseif generator.generator == "tagged" then
75    room = hyper.rooms.make_tagged_room(generator,options)
76  end
77
78  if hyper.profile then
79    profiler.pop()
80    profiler.push("AnalyseRoom")
81  end
82
83  if room == nil then return nil end
84
85  -- Do we analyse this room? Usually necessary but we might not need
86  -- it e.g. random placement layouts where it doesn't matter so much
87  local analyse = true
88  if generator.analyse ~= nil then analyse = generator.analyse end
89
90  if analyse then
91    hyper.rooms.analyse_room(room,options)
92  end
93
94  if hyper.profile then
95    profiler.pop()
96    profiler.push("TransformRoom")
97  end
98
99  -- Optionally transform the room. This is used to add walls for V
100  -- and other layouts but a transform doesn't have to be provided.
101  local transform = generator.room_transform or options.room_transform
102  if transform ~= nil then
103    room = transform(room,options)
104    if perform_analysis then
105      hyper.rooms.analyse_room(room,options)
106    end
107  end
108
109  if hyper.profile then
110    profiler.pop()
111    profiler.push("AnalyseRoomInternal")
112  end
113
114  if generator.analyse_interal then
115    hyper.usage.analyse_grid_usage(room.grid,options)
116  end
117
118  if hyper.profile then
119    profiler.pop()
120  end
121
122  if hyper.debug then
123    dump_usage_grid_v3(room.grid)
124  end
125
126  -- 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.
127  -- 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
128  --       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
129  --       generating rooms is one of the more expensive operations now. Really before making any assumptions like that I should run
130  --       some proper analysis of the expensiveness of different operations and get a good picture of what needs to be optimised.
131  --       Perhaps it's still possible to find some ways to move expensive parts out to C++ (e.g. mask and normal generation, placement itself)
132  --       whilst retaining callbacks.
133  room.id = options.rooms_placed
134
135  return room
136end
137
138function hyper.rooms.make_code_room(chosen,options)
139  -- Pick a size for the room
140  local size
141  if chosen.size ~= nil then
142    if type(chosen.size) == "function" then
143      -- Callback function
144      size = chosen.size(options,chosen)
145    else
146      -- Static size
147      size = chosen.size
148    end
149  else
150    -- Default size function
151    local size_func = chosen.size_callback or hyper.rooms.size_default
152    size = size_func(chosen,options)
153  end
154
155  room = {
156    type = "grid",
157    size = size,
158    generator_used = chosen,
159    grid = hyper.usage.new_usage(size.x,size.y)
160  }
161
162  -- Paint and decorate the layout grid
163  hyper.paint.paint_grid(chosen.paint_callback(room,options,chosen),options,room.grid)
164  if chosen.decorate_callback ~= nil then chosen.decorate_callback(room.grid,room,options) end  -- Post-production
165
166  return room
167end
168
169-- Pick a map by tag and make a room grid from it
170function hyper.rooms.make_tagged_room(chosen,options)
171  -- Resolve a map with the specified tag
172  local mapdef = dgn.map_by_tag(chosen.tag,true)
173  if mapdef == nil then return nil end  -- Shouldn't happen when thing are working but just in case
174
175  -- Temporarily prevent map getting mirrored / rotated during resolution because it'll seriously
176  -- screw with our attempts to understand and position the vault later; and hardwire transparency because lack of it can fail a whole layout
177  -- TODO: Store these tags on the room first so we can actually support them down the line ...
178  dgn.tags(mapdef, "no_vmirror no_hmirror no_rotate transparent");
179  -- Resolve the map so we can find its width / height
180  local map, vplace = dgn.resolve_map(mapdef,false)
181  local room,room_width,room_height
182
183    -- If we can't find a map then we're probably not going to find one
184  if map == nil then return nil end
185  -- Allow map to be flipped and rotated again, otherwise we'll struggle later when we want to rotate it into the correct orientation
186  dgn.tags_remove(map, "no_vmirror no_hmirror no_rotate")
187
188  local room_width,room_height = dgn.mapsize(map)
189  local veto = false
190  -- Check min/max room sizes are observed
191  if (chosen.min_size == nil or not (room_width < chosen.min_size or room_height < chosen.min_size))
192    and (chosen.max_size == nil or not (room_width > chosen.max_size or room_height > chosen.max_size)) then
193
194    room = {
195      type = "vault",
196      size = { x = room_width, y = room_height },
197      map = map,
198      vplace = vplace,
199      generator_used = chosen,
200      grid = hyper.usage.new_usage(room_width,room_height)
201    }
202
203    room.preserve_wall = dgn.has_tag(room.map, "preserve_wall")
204    room.no_windows = dgn.has_tag(room.map, "no_windows")
205
206    -- Check all four directions for orient tag before we create the wals data, since the existence of a
207    -- single orient tag makes the other walls ineligible
208    room.tags = {}
209    for n = 0, 3, 1 do
210      if dgn.has_tag(room.map,"vaults_orient_" .. vector.normals[n+1].name) then
211        room.has_orient = true
212        room.tags[n] = true
213      end
214    end
215    -- Make usage grid by feature inspection; will be used to compute wall data
216    for m = 0, room.size.y - 1, 1 do
217      for n = 0, room.size.x - 1, 1 do
218        local inspected = { }
219        inspected.feature, inspected.exit, inspected.space = dgn.inspect_map(vplace,n,m)
220        inspected.solid = not (feat.has_solid_floor(feature) or feat.is_door(feature))
221        inspected.feature = dgn.feature_name(inspected.feature)
222        hyper.usage.set_usage(room.grid,n,m,inspected)
223      end
224    end
225    found = true
226  end
227  return room
228end
229
230-- Analyse the exit squares of a room to determine connectability / eligibility for placement.
231function hyper.rooms.analyse_room(room,options)
232  -- Initialise walls table
233  room.walls = { }
234  for n = 0, 3, 1 do
235    room.walls[n] = { eligible = false }
236  end
237
238  local inspect_cells = {}
239  local has_exits = false
240
241  if hyper.profile then
242    profiler.push("FirstPass")
243  end
244
245  for m = 0, room.size.y-1, 1 do
246    for n = 0, room.size.x-1, 1 do
247      local cell = hyper.usage.get_usage(room.grid,n,m)
248
249      -- If we find an exit then flag this, causing non-exit edge cells to get ignored
250      if cell.exit then has_exits = true end
251      cell.anchors = {}
252      -- Remember if the cell is a relevant connector cell
253      if not cell.space and (cell.carvable or not cell.solid) then
254        table.insert(inspect_cells,{ cell = cell, pos = { x = n, y = m } } )
255      end
256    end
257  end
258
259  if hyper.profile then
260    profiler.pop()
261    profiler.push("SecondPass")
262  end
263
264  -- Loop through the cells again now we know has_exits, and store all connected cells in our list of walls for later.
265  for i,inspect in ipairs(inspect_cells) do
266    local cell = inspect.cell
267    local n,m = inspect.pos.x,inspect.pos.y
268
269    if not has_exits or cell.exit then
270      -- Analyse squares around it
271      for i,normal in ipairs(room.allow_diagonals and vector.directions or vector.normals) do
272        local near_pos = { x = n + normal.x,y = m + normal.y }
273        local near_cell = hyper.usage.get_usage(room.grid,near_pos.x,near_pos.y)
274        if near_cell == nil or near_cell.space then
275          -- This is a possible anchor
276          local anchor = true
277          local target = inspect.pos
278          local anchor_pos = normal
279          -- If it's a wall we need space on the opposite side
280          if cell.solid then
281            anchor = false
282            if cell.carvable then
283              local target_cell = hyper.usage.get_usage(room.grid,normal.x - n,normal.y - m)
284              if target_cell ~= nil and not target_cell.solid then
285                anchor_pos = { x = 0, y = 0 }
286                anchor = true
287              end
288            end
289          end
290          if anchor then
291            -- Record this is as a possible anchor
292            table.insert(cell.anchors, { normal = normal, pos = anchor_pos, origin = inspect.pos })
293            -- table.insert(room.walls[normal.dir],{ cell = cell, pos = inspect.pos, normal = normal } )
294            -- room.walls[normal.dir].eligible = true
295            cell.connected = true
296         end
297        end
298      end
299      hyper.usage.set_usage(room.grid,n,m,cell)
300    end
301  end
302
303  if hyper.profile then
304    profiler.pop()
305  end
306end
307
308-- Transforms a room by returning a new room sized 2 bigger with walls added next to all open edge squares
309function hyper.rooms.add_walls(room, options)
310
311  local walled_room = {
312    type = "transform",
313    size = { x = room.size.x + 2, y = room.size.y + 2 },
314    generator_used = room.generator_used,
315    transform = "add_walls",
316    flags = room.flags,
317    inner_room = room,
318    inner_room_pos = { x = 1, y = 1 }
319  }
320
321  walled_room.grid = hyper.usage.new_usage(walled_room.size.x,walled_room.size.y)
322  -- Loop through all the squares and add the walls
323  for m = 0, walled_room.size.y-1, 1 do
324    for n = 0, walled_room.size.x-1, 1 do
325      -- Get corresponding square from original room
326      local usage = hyper.usage.get_usage(room.grid,n-1,m-1)
327      if usage == nil then usage = { space = true } end
328      -- If it's not space, copy usage from the inner room
329      if not usage.space then
330        usage.inner = true
331        hyper.usage.set_usage(walled_room.grid,n,m,usage)
332      else
333        -- Otherwise check if we're bordering any open squares and therefore need to draw a wall.
334        local any_open = false
335        for i,normal in ipairs(vector.directions) do
336          local near_cell = hyper.usage.get_usage(room.grid,n+normal.x-1,m+normal.y-1)
337          if near_cell == nil then near_cell = { space = true } end
338          if not near_cell.space and not near_cell.solid then
339            any_open = true
340            break
341          end
342        end
343        if any_open then
344          -- There was at least one open square so we need to make a wall which *could* be carvable.
345          -- Note: carvable doesn't mean this can necessarily be used as a door, e.g. Fort crennelations ... that will still happen in room analysys. It
346          -- makes the logic overall much simpler.
347          -- TODO: Allow diagonal doors here too. Diagonals are problematic (in the previous loop and in analyse_room) because we'd get overlapping anchors
348          --       with adjacent walls, this really doesn't sound good.
349          local wall_usage = { feature = "rock_wall", wall = true, protect = true }
350          for i,normal in ipairs(vector.normals) do
351            local near = hyper.usage.get_usage(room.grid,n+normal.x-1,m+normal.y-1)
352            if near ~= nil and near.connected then
353              wall_usage.carvable = true
354              wall_usage.connected = true  -- TODO: Might be redundant
355            end
356          end
357          hyper.usage.set_usage(walled_room.grid, n, m, wall_usage)
358        end
359      end
360    end
361  end
362
363  return walled_room
364end
365
366function hyper.rooms.add_buffer(room, options)
367
368  local walled_room = {
369    type = "transform",
370    size = { x = room.size.x + 2, y = room.size.y + 2 },
371    generator_used = room.generator_used,
372    transform = "add_buffer",
373    flags = room.flags,
374  }
375
376  -- Take on the existing inner room if available (wall+buffer)
377  walled_room.inner_room = room.inner_room or room
378  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 }
379  walled_room.grid = hyper.usage.new_usage(walled_room.size.x,walled_room.size.y)
380
381  -- Loop through all the squares and add the buffer
382  for m = 0, walled_room.size.y-1, 1 do
383    for n = 0, walled_room.size.x-1, 1 do
384      -- Get corresponding square from original room
385      local usage = hyper.usage.get_usage(room.grid,n-1,m-1)
386      if usage == nil then usage = { space = true } end
387      -- If it's not space, copy usage from the inner room
388      if not usage.space then
389        usage.inner = true
390        hyper.usage.set_usage(walled_room.grid,n,m,usage)
391      else
392        -- Otherwise check if we're bordering any non-space and therefore need to create a buffer
393        local any_open = false
394        for i,normal in ipairs(vector.directions) do
395          local near_cell = hyper.usage.get_usage(room.grid,n+normal.x-1,m+normal.y-1)
396          if near_cell == nil then near_cell = { space = true } end
397          if not near_cell.space then
398            any_open = true
399            break
400          end
401        end
402        if any_open then
403          hyper.usage.set_usage(walled_room.grid, n, m, { space = true, buffer = true })
404        end
405      end
406    end
407  end
408
409  return walled_room
410end
411
412function hyper.rooms.add_buffer_walls(room, options)
413  return hyper.rooms.add_buffer(hyper.rooms.add_walls(room,options),options)
414end
415