File: v_layouts.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 (385 lines) | stat: -rw-r--r-- 16,008 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
------------------------------------------------------------------------------
-- v_layouts.lua:
--
-- Main layouts include for Vaults.
--
-- Code by mumra
-- Based on work by infiniplex, based on original designs and code by mumra.
--
-- Contains the functions that actually build the layouts.
--
-- Notes:
-- The original had the entire layout code in layout_vaults.des under a
-- single map def. This made things very hard to understand. Here I am
-- separating things into some smaller separate which can be called
-- from short map headers in layout_vaults.des.
--
-- The layout functions set up an array of floor/wall areas then call the paint
-- functions and room building functions. It'd be easy to allow more complex
-- geometries by passing in callbacks or enhancing the paint function but in
-- general we only want wide straight corridors or open rectangular areas so
-- room/door placement can work correctly.
------------------------------------------------------------------------------

hypervaults = {}  -- Old namespace, gradually moving everything into hyper.*

crawl_require("dlua/v_debug.lua")
crawl_require("dlua/v_paint.lua")
crawl_require("dlua/v_rooms.lua")
crawl_require("dlua/v_shapes.lua")

_VAULTS_DEBUG = false

-- The four directions and their associated vector normal and name.
-- This helps us manage orientation of rooms.
hypervaults.normals = {
  { x = 0, y = -1, dir = 0, name="n" },
  { x = -1, y = 0, dir = 1, name="w" },
  { x = 0, y = 1, dir = 2, name="s" },
  { x = 1, y = 0, dir = 3, name="e" }
}

-- Default parameters for all Vaults layouts. Some individual layouts might tweak these parameters to create specific effects.
function vaults_default_options()

  -- Decide weights for wall types based on depth
  local depth = you.depth()
  local stone_weight = (4-depth) * 10              -- 30,20,10,0
  local metal_weight = math.max(0,(depth-2) * 20)  -- 0,0,20,40
  local crystal_weight = math.max(0,depth-2)       -- 0,0,1,2

  -- Main options table
  local options = {
    max_rooms = 27, -- Maximum number of rooms to attempt to place
    max_room_depth = 0, -- No max depth (not implemented yet anyway)
    min_distance_from_wall = 2, -- Room must be at least this far from outer walls (in open areas). Reduces chokepoints.
    -- The following settings (in addition to max_rooms) can adjust how long it takes to build a level, at the expense of potentially less intereting layouts
    max_room_tries = 20, -- How many *consecutive* rooms can fail to place before we exit the entire routine
    max_place_tries = 50, -- How many times we will attempt to place *each room* before picking another

    -- Special option for Vaults (handled in hyper.vaults.floor_vault) - chance out of 100 of placing a stair somewhere in the middle of the floor vault
    stair_chance = 80,

    -- Weightings of various types of room generators.
    room_type_weights = {
      { generator = "code", paint_callback = hypervaults.shapes.floor_vault, weight = 80, min_size = 6, max_size = 15 }, -- Floor vault
      { generator = "tagged", tag = "vaults_room", weight = 50, max_rooms = 6 },
      { generator = "tagged", tag = "vaults_empty", weight = 40 },
      { generator = "tagged", tag = "vaults_hard", weight = 10, max_rooms = 1 },
      { generator = "tagged", tag = "vaults_entry_crypt", weight = (you.where() == dgn.level_name(dgn.br_entrance("Crypt"))) and 25 or 0, max_rooms = 1 },
      -- Create tagged generators to represent portal entry vaults. Weights
      -- should be 0 since these generators are only chosen through chance rolls.
      { generator = "tagged", tag = "vaults_necropolis", weight = 0, max_rooms = 1 },
      { generator = "tagged", tag = "vaults_wizlab", weight = 0, max_rooms = 1 },
      { generator = "tagged", tag = "vaults_desolation", weight = 0, max_rooms = 1 },
-- start TAG_MAJOR_VERSION == 34
      { generator = "tagged", tag = "vaults_entry_forest", weight = (you.where() == dgn.level_name(dgn.br_entrance("Forest"))) and 25 or 0, max_rooms = 1 },
      { generator = "tagged", tag = "vaults_entry_blade", weight = (you.where() == dgn.level_name(dgn.br_entrance("Blade"))) and 25 or 0, max_rooms = 1 },
-- end TAG_MAJOR_VERSION
    },

    -- Weightings for types of wall to use across the whole layout
    layout_wall_weights = {
      { feature = "stone_wall", weight = stone_weight },
      { feature = "metal_wall", weight = metal_weight },
      { feature = "crystal_wall", weight = crystal_weight },
    },
    layout_floor_type = "floor"

  }

  return options
end

-- Default options table
function hypervaults.default_options()

  local options = {
    max_rooms = 27, -- Maximum number of rooms to attempt to place
    max_room_depth = 0, -- No max depth (not implemented yet anyway)
    min_distance_from_wall = 2, -- Room must be at least this far from outer walls (in open areas). Reduces chokepoints.
    -- The following settings (in addition to max_rooms) can adjust how long it takes to build a level, at the expense of potentially less intereting layouts
    max_room_tries = 27, -- How many *consecutive* rooms can fail to place before we exit the entire routine
    max_place_tries = 50, -- How many times we will attempt to place *each room* before picking another

    -- Weightings of various types of room generators. The plan is to better support code vaults here.
    room_type_weights = {
      { generator = "code", paint_callback = floor_vault_paint_callback, weight = 1, min_size = 4, max_size = 40, empty = true }, -- Floor vault
    },

    -- Rock seems a sensible default
    layout_wall_weights = {
      { feature = "rock_wall", weight = 1 },
    },
    layout_floor_type = "floor"  -- Should probably never need to be anything else

  }

  return options

end

-- TODO: The 'paint' parameter should disappear. Instead the options will contain a setup array. This could contain paint instructions,
-- but alternately should be able to wire room generators to create initial terrain. Since rooms can now be created from paint arrays _anyway_,
-- it seems that pre-painting the layout is completely unnecessary and room generation can do anything ...
function hypervaults.build_layout(e, name, paint, options)
  if e.is_validating() then return; end

  if _VAULTS_DEBUG then print("Hypervaults Layout: " .. name) end

  local default_options = hypervaults.default_options()
  if options ~= null then hypervaults.merge_options(default_options,options) end

  local data = paint_vaults_layout(paint, default_options)
  local rooms = place_vaults_rooms(e, data, default_options.max_rooms, default_options)
end

-- Merges together two options tables (usually the default options getting
-- overwritten by a minimal set provided by an individual layout)
function hypervaults.merge_options(base,to_merge)
  -- Quite simplistic, right now this won't recurse into sub tables (it might need to)
  for key, val in pairs(to_merge) do
    base[key] = val
  end
  return base
end

-- Build any vaults layout from a paint array, useful to quickly prototype
-- layouts in layout_vaults.des
function build_vaults_layout(e, name, paint, options)
  if e.is_validating() then return; end

  if _VAULTS_DEBUG then print("Vaults Layout: " .. name) end

  local defaults = vaults_default_options()
  if options ~= nil then hypervaults.merge_options(defaults,options) end

  local data = paint_vaults_layout(paint, defaults)
  local rooms = place_vaults_rooms(e, data, defaults.max_rooms, defaults)
end

function build_vaults_ring_layout(e, corridorWidth, outerPadding)

  local gxm, gym = dgn.max_bounds()

  local c1 = { x = outerPadding, y = outerPadding }
  local c2 = { x = outerPadding + corridorWidth, y = outerPadding + corridorWidth }
  local c3 = { x = gxm - outerPadding - corridorWidth - 1, y = gym - outerPadding - corridorWidth - 1 }
  local c4 = { x = gxm - outerPadding - 1, y = gym - outerPadding - 1 }

  -- Paint four floors
  local paint = {
    { type = "floor", corner1 = c1, corner2 = c4},
    { type = "wall", corner1 = c2, corner2 = c3 }
  }

  build_vaults_layout(e, "Ring", paint, { max_room_depth = 3 })

end

function build_vaults_cross_layout(e, corridorWidth, intersect)

  -- Ignoring intersect for now
  local gxm, gym = dgn.max_bounds()

  local corridorWidth = 3 + crawl.random2avg(3,2)

  local xc = math.floor((gxm-corridorWidth)/2)
  local yc = math.floor((gym-corridorWidth)/2)
  local paint = {
    { type = "floor", corner1 = { x = xc, y = 1 }, corner2 = { x = xc + corridorWidth - 1, y = gym - 2 } },
    { type = "floor", corner1 = { x = 1, y = yc }, corner2 = { x = gxm - 2, y = yc + corridorWidth - 1 } }
  }

  build_vaults_layout(e, "Cross", paint, { max_room_depth = 4 })

end

function build_vaults_big_room_layout(e, minPadding,maxPadding)
  -- The Big Room
  local gxm, gym = dgn.max_bounds()
  local padx,pady = crawl.random_range(minPadding,maxPadding),crawl.random_range(minPadding,maxPadding)

  -- Will have a ring of outer rooms but the central area will be chaotic city
  local paint = {
    { type = "floor", corner1 = { x = padx, y = pady }, corner2 = { x = gxm - padx - 1, y = gym - pady - 1 } }
  }
  build_vaults_layout(e, "Big Room", paint, { max_room_depth = 4, max_rooms = 23, max_room_tries = 12, max_place_tries = 30 })

end

function build_vaults_chaotic_city_layout(e)
  local gxm, gym = dgn.max_bounds()

  -- Paint entire level with floor
  local paint = {
    { type = "floor", corner1 = { x = 1, y = 1 }, corner2 = { x = gxm - 2, y = gym - 2 } }
  }

  build_vaults_layout(e, "Chaotic City", paint, { max_room_depth = 5, max_rooms = 25, max_room_tries = 10, max_place_tries = 20 })

end

function build_vaults_maze_layout(e,veto_callback, name)
  local gxm, gym = dgn.max_bounds()
  if name == nil then name = "Maze" end

  -- Put a single empty room somewhere roughly central. All rooms will be built off from each other following this
  local x1 = crawl.random_range(30, gxm-70)
  local y1 = crawl.random_range(30, gym-70)

  local paint = {
    { type = "floor", corner1 = { x = x1, y = y1 }, corner2 = { x = x1 + crawl.random_range(4,10), y = y1 + crawl.random_range(4,10) } }
  }

  build_vaults_layout(e, name, paint, { max_room_depth = 0, veto_place_callback = veto_callback })

end
-- Uses a custom veto function to alternate between up or down connections every {dist} rooms
function build_vaults_maze_snakey_layout(e)

  local which = 0
  if crawl.coinflip() then which = 1 end

  -- How many rooms to draw before alternating
  -- When it's 1 it doesn't create an obvious visual effect but hopefully in gameplay
  -- it will be more obvious.
  -- TODO: This range can be higher if the callback has the coords (which it now does on state.pos or state.base),
  --       if we're near the edge then bailout.
  local dist = crawl.random_range(1,2)
  -- Just to be fair we'll offset by a number, this will determine how far from the
  -- first room it goes before the first switch
  local offset = crawl.random_range(0,dist-1)
  -- Callback function
  local function callback(state)
    if state.usage.normal == nil or state.usage.depth == nil then return false end
    local odd = (math.floor(state.usage.depth/dist)+offset) % 2
    if odd == which then
      if state.usage.normal.y == 0 then return true end
      return false
    end
    if state.usage.normal.x == 0 then return true end
    return false
  end
  build_vaults_maze_layout(e, callback, "Snakey Maze")
end

-- Goes just either horizontally or vertically for a few rooms then branches out, makes
-- an interesting sprawly maze with two bulby areas
function build_vaults_maze_bifur_layout(e)

  local which = crawl.coinflip()
  local target_depth = crawl.random_range(2,3) -- TODO: This range can be higher if the callback has the coords, if we're near the edge then bailout

  -- Callback function
  local function callback(state)
    if state.usage.normal == nil then return false end
    if state.usage.depth == nil or state.usage.depth > target_depth then return false end
    -- TODO: Should also check if the coord is within a certain distance of map edge as an emergency bailout
    -- ... but we don't have the coord here right now
    if which then
      if state.usage.normal.y == 0 then
        return true
      end
      return false
    end
    if state.usage.normal.x == 0 then
      return true
    end
    return false

  end
  build_vaults_maze_layout(e,callback, "Bifur Maze")
end
-- TODO: Spirally maze

-- Builds the paint array for omnigrid
function layout_primitive_omnigrid()

  local gxm,gym = dgn.max_bounds()
  local options = {
    subdivide_initial_chance = 100, -- % chance of subdividing at first level, if < 100 then we might just get chaotic city
    subdivide_level_multiplier = 0.80,   -- Multiply subdivide chance by this amount with each level
    minimum_size = 15,  -- Don't ever create areas smaller than this
    fill_chance = 64, -- % chance of filling an area vs. leaving it open
    fill_padding = 2,  -- Padding around a fill area, effectively this is half the corridor width
    jitter_min = -1,
    jitter_max = 2
  }
  local results = omnigrid_subdivide_area(1,1,gxm-2,gym-2,options)
  local paint = {}
  local jitter = crawl.coinflip()
  for i, area in ipairs(results) do
    table.insert(paint,{ type = "floor", corner1 = {x = area.x1, y = area.y1}, corner2 = { x = area.x2, y = area.y2 }})
    -- Fill the area? -- TODO: We should do something to ensure at least one area is filled. Otherwise it's just another chaotic_city ...
    if crawl.random2(100) < options.fill_chance then
      local corner1 = { x = area.x1 + options.fill_padding, y = area.y1 + options.fill_padding }
      local corner2 = { x = area.x2 - options.fill_padding, y = area.y2 - options.fill_padding }
      -- Perform jitter
      if jitter then
        corner1.x = corner1.x + crawl.random_range(options.jitter_min,options.jitter_max)
        corner1.y = corner1.y + crawl.random_range(options.jitter_min,options.jitter_max)
        corner2.x = corner2.x - crawl.random_range(options.jitter_min,options.jitter_max)
        corner2.y = corner2.y - crawl.random_range(options.jitter_min,options.jitter_max)
      end

      table.insert(paint,{ type = "wall", corner1 = corner1, corner2 = corner2 } )
    end
  end
  return paint
end

function omnigrid_subdivide_area(x1,y1,x2,y2,options,results,chance)
  -- Default parameters
  if results == nil then results = { } end
  if chance == nil then chance = options.subdivide_initial_chance end

  local subdiv_x, subdiv_y, subdivide = true,true,true
  local width,height = x2-x1+1,y2-y1+1

  -- Check which if any axes can be subdivided

  if width < 2 * options.minimum_size then
    subdiv_x = false
  end
  if height < 2 * options.minimum_size then
    subdiv_y = false
  end
  if not subdiv_x and not subdiv_y then
    subdivide = false
  end

  if crawl.random2(100) >= chance then
    subdivide = false
  end

  if not subdivide then
    -- End of subdivision; add an area
    table.insert(results, { x1=x1,y1=y1,x2=x2,y2=y2 })
  else
    -- Choose axis? (Remember some might already be too small)
    local which = "x"
    if not subdiv_x then which = "y"
    elseif subdiv_y then
      if crawl.coinflip() then which = "y" end
    end

    local new_chance = chance * options.subdivide_level_multiplier

    -- Could probably avoid this duplication but it's not that bad
    if which == "x" then
      local pos = crawl.random_range(options.minimum_size,width-options.minimum_size)
      -- Create the two new areas
      omnigrid_subdivide_area(x1,y1,x1 + pos - 1,y2,options,results,new_chance)
      omnigrid_subdivide_area(x1 + pos,y1,x2,y2,options,results,new_chance)
    else
      local pos = crawl.random_range(options.minimum_size,height-options.minimum_size)
      -- Create the two new areas
      omnigrid_subdivide_area(x1,y1,x2,y1 + pos - 1,options,results,new_chance)
      omnigrid_subdivide_area(x1,y1 + pos,x2,y2,options,results,new_chance)
    end
  end

  return results

end