ludic/packages/ludic.dungeon/dungeon.ludic

126 lines
6 KiB
Text

# dungeon.ludic — `Dungeon.*`: arena rooms for a room-to-room roguelite, built
# straight into the engine tilemap (Map.*).
#
# Dungeon.arena(style, wall, floor) a walled room with mirrored cover in one of
# the RoomStyles, and the door lanes (two cells
# wide, through the centre) always open
# Dungeon.open_arena(wall, floor) the walled room alone (a boss arena)
# Dungeon.random_style() a style roll (Scatter twice as likely)
# Dungeon.set_exit(side, glyph) the two centre cells of one edge
# Dungeon.entry_point(side, inset, w, h) where a w x h body starts after entering
# Dungeon.opposite(side) the side across the room
# Dungeon.at_edge(tile) is a tile on the border? (an open doorway is
# the only border cell a body can stand in)
#
# Cover never touches the two-tile margin, and every style mirrors across both axes,
# so every quadrant stays reachable. Deterministic: the seeded RNG drives it all.
enum Side { North, South, West, East } # opposite = side ^ 1
enum RoomStyle { Pillars, Columns, CenterSlab, Bars, Scatter }
const DUNGEON_MARGIN: int = 2
state DungeonState {
dungeon_wall: int = '#'
dungeon_floor: int = '.'
}
function dungeon_columns() -> int { return Map.width() }
function dungeon_rows() -> int { return Map.height() }
function dungeon_door_column() -> int { return Map.width() / 2 - 1 } # the left of the two door cells
function dungeon_door_row() -> int { return Map.height() / 2 } # the top of the two door cells
# a wall, but never in the margin so rooms stay traversable
function dungeon_place(dungeon_st: DungeonState, x: int, y: int) -> void {
if (x < DUNGEON_MARGIN) or (x > dungeon_columns() - 1 - DUNGEON_MARGIN) { return }
if (y < DUNGEON_MARGIN) or (y > dungeon_rows() - 1 - DUNGEON_MARGIN) { return }
Map.set(x: x, y: y, glyph: dungeon_st.dungeon_wall)
}
function dungeon_mirrored(dungeon_st: DungeonState, x: int, y: int) -> void {
dungeon_place(dungeon_st, x, y)
dungeon_place(dungeon_st, dungeon_columns() - 1 - x, y)
dungeon_place(dungeon_st, x, dungeon_rows() - 1 - y)
dungeon_place(dungeon_st, dungeon_columns() - 1 - x, dungeon_rows() - 1 - y)
}
function dungeon_pillar(dungeon_st: DungeonState, x: int, y: int) -> void { # 2x2, mirrored four ways
dungeon_mirrored(dungeon_st, x, y)
dungeon_mirrored(dungeon_st, x + 1, y)
dungeon_mirrored(dungeon_st, x, y + 1)
dungeon_mirrored(dungeon_st, x + 1, y + 1)
}
function dungeon_style(dungeon_st: DungeonState, style: int) -> void {
let cols = dungeon_columns()
let rows = dungeon_rows()
match style {
RoomStyle.Pillars => {
let x = Random.range(low: 3, high: 5)
let y = Random.range(low: 3, high: 4)
dungeon_pillar(dungeon_st, x, y)
if Random.chance(percent: 60) { dungeon_pillar(dungeon_st, x + 3, y + 2) }
}
RoomStyle.Columns => {
let x = Random.range(low: 4, high: 6)
for y in 3 .. rows - 3 { dungeon_mirrored(dungeon_st, x, y) }
}
RoomStyle.CenterSlab => { Map.rect(x: 6, y: 5, width: cols - 12, height: rows - 10, glyph: dungeon_st.dungeon_wall) }
RoomStyle.Bars => {
let y = Random.range(low: 3, high: 4)
for x in 4 .. cols - 4 { dungeon_mirrored(dungeon_st, x, y) }
}
_ => {
for i in 0 .. Random.range(low: 3, high: 5) {
let x = Random.range(low: 3, high: cols / 2 - 1)
dungeon_place(dungeon_st, x, Random.range(low: 3, high: rows - 4))
dungeon_place(dungeon_st, cols - 1 - x, Random.range(low: 3, high: rows - 4))
}
}
}
}
@Namespace(Dungeon) function dungeon_open_arena(dungeon_st: mut DungeonState, wall: int, floor: int) -> void {
dungeon_st.dungeon_wall = wall
dungeon_st.dungeon_floor = floor
Map.fill(glyph: floor)
Map.border(glyph: wall)
}
@Namespace(Dungeon) function dungeon_arena(dungeon_st: mut DungeonState, style: int, wall: int, floor: int) -> void {
dungeon_open_arena(dungeon_st, wall, floor)
dungeon_style(dungeon_st, style)
Map.rect(x: dungeon_door_column(), y: 1, width: 2, height: dungeon_rows() - 2, glyph: floor) # the door lanes
Map.rect(x: 1, y: dungeon_door_row(), width: dungeon_columns() - 2, height: 2, glyph: floor)
}
@Namespace(Dungeon) function dungeon_random_style() -> int { return Random.range(low: 0, high: 5) }
# the first of an exit's two cells, and the step to the second
function dungeon_exit_cell(side: int) -> IVec2 {
match side {
Side.North => { return IVec2.make(dungeon_door_column(), 0) }
Side.South => { return IVec2.make(dungeon_door_column(), dungeon_rows() - 1) }
Side.West => { return IVec2.make(0, dungeon_door_row()) }
_ => { return IVec2.make(dungeon_columns() - 1, dungeon_door_row()) }
}
}
@Namespace(Dungeon) function dungeon_set_exit(side: int, glyph: int) -> void {
let first = dungeon_exit_cell(side)
var second = IVec2.add(first, IVec2.make(0, 1))
if side <= Side.South { second = IVec2.add(first, IVec2.make(1, 0)) }
Map.set(x: first.x, y: first.y, glyph: glyph)
Map.set(x: second.x, y: second.y, glyph: glyph)
}
# where a body of w x h px starts after entering from `side`, `inset` px inside the edge
@Namespace(Dungeon) function dungeon_entry_point(side: int, inset: int, w: int, h: int) -> IVec2 {
let tile = rt_map_tile_size()
let across_x = (dungeon_door_column() + 1) * tile - w / 2
let across_y = dungeon_door_row() * tile + (2 * tile - h) / 2
match side {
Side.North => { return IVec2.make(across_x, inset) }
Side.South => { return IVec2.make(across_x, dungeon_rows() * tile - inset - h) }
Side.West => { return IVec2.make(inset, across_y) }
_ => { return IVec2.make(dungeon_columns() * tile - inset - w, across_y) }
}
}
@Namespace(Dungeon) function dungeon_opposite(side: int) -> int { return side ^ 1 }
@Namespace(Dungeon) function dungeon_at_edge(tile: IVec2) -> bool {
return (tile.x == 0) or (tile.y == 0) or (tile.x == dungeon_columns() - 1) or (tile.y == dungeon_rows() - 1)
}