Tavern room layout

This commit is contained in:
Isse 2023-10-14 20:28:20 +02:00
parent 63e500d3d8
commit ecb85a0534
6 changed files with 605 additions and 10 deletions

View File

@ -113,6 +113,7 @@ impl Data {
PlotKind::House(_)
| PlotKind::Workshop(_)
| PlotKind::AirshipDock(_)
| PlotKind::Tavern(_)
| PlotKind::Plaza
| PlotKind::SavannahPit(_)
| PlotKind::SavannahHut(_)

View File

@ -539,6 +539,7 @@ impl Civs {
let size = Lerp::lerp(0.03, 1.0, rng.gen_range(0.0..1f32).powi(5));
WorldSite::refactor(site2::Site::generate_city(
&Land::from_sim(ctx.sim),
index_ref,
&mut rng,
wpos,
size,

View File

@ -573,7 +573,13 @@ impl Site {
}
// Size is 0..1
pub fn generate_city(land: &Land, rng: &mut impl Rng, origin: Vec2<i32>, size: f32) -> Self {
pub fn generate_city(
land: &Land,
index: IndexRef,
rng: &mut impl Rng,
origin: Vec2<i32>,
size: f32,
) -> Self {
let mut rng = reseed(rng);
let mut site = Site {
@ -587,12 +593,13 @@ impl Site {
site.make_plaza(land, &mut rng);
let build_chance = Lottery::from(vec![
(64.0, 1),
(5.0, 2),
(8.0, 3),
(5.0, 4),
(5.0, 5),
(15.0, 6),
// (64.0, 1),
// (5.0, 2),
// (8.0, 3),
// (5.0, 4),
// (5.0, 5),
// (15.0, 6),
(15.0, 7),
]);
let mut castles = 0;
@ -917,6 +924,42 @@ impl Site {
}
}
},
7 if size > 0.125 => {
let size = (3.5 + rng.gen::<f32>().powf(5.0) * 2.0).round() as u32;
if let Some((aabr, door_tile, door_dir)) = attempt(32, || {
site.find_roadside_aabr(
&mut rng,
7..(size + 1).pow(2),
Extent2::broadcast(size),
)
}) {
let tavern = plot::Tavern::generate(
land,
index,
&mut reseed(&mut rng),
&site,
door_tile,
Dir::from_vector(door_dir),
aabr,
);
let tavern_alt = tavern.door_wpos.z;
let plot = site.create_plot(Plot {
kind: PlotKind::Tavern(tavern),
root_tile: aabr.center(),
tiles: aabr_tiles(aabr).collect(),
seed: rng.gen(),
});
site.blit_aabr(aabr, Tile {
kind: TileKind::Building,
plot: Some(plot),
hard_alt: Some(tavern_alt),
});
workshops += 1;
} else {
site.make_plaza(land, &mut rng);
}
},
_ => {},
}
}
@ -1824,6 +1867,7 @@ impl Site {
let (prim_tree, fills, mut entities) = match &self.plots[plot].kind {
PlotKind::House(house) => house.render_collect(self, canvas),
PlotKind::AirshipDock(airship_dock) => airship_dock.render_collect(self, canvas),
PlotKind::Tavern(tavern) => tavern.render_collect(self, canvas),
PlotKind::CoastalHouse(coastal_house) => coastal_house.render_collect(self, canvas),
PlotKind::CoastalWorkshop(coastal_workshop) => {
coastal_workshop.render_collect(self, canvas)
@ -1963,7 +2007,19 @@ impl Site {
}
pub fn test_site() -> Site {
Site::generate_city(&Land::empty(), &mut thread_rng(), Vec2::zero(), 0.5)
let index = crate::index::Index::new(0);
let index_ref = IndexRef {
colors: &index.colors(),
features: &index.features(),
index: &index,
};
Site::generate_city(
&Land::empty(),
index_ref,
&mut thread_rng(),
Vec2::zero(),
0.5,
)
}
fn wpos_is_hazard(land: &Land, wpos: Vec2<i32>) -> Option<HazardKind> {

View File

@ -22,6 +22,7 @@ mod savannah_hut;
mod savannah_pit;
mod savannah_workshop;
mod sea_chapel;
mod tavern;
mod troll_cave;
mod workshop;
@ -34,7 +35,7 @@ pub use self::{
gnarling::GnarlingFortification, house::House, jungle_ruin::JungleRuin,
pirate_hideout::PirateHideout, rock_circle::RockCircle, savannah_hut::SavannahHut,
savannah_pit::SavannahPit, savannah_workshop::SavannahWorkshop, sea_chapel::SeaChapel,
troll_cave::TrollCave, workshop::Workshop,
tavern::Tavern, troll_cave::TrollCave, workshop::Workshop,
};
use super::*;
@ -77,6 +78,7 @@ impl Plot {
pub enum PlotKind {
House(House),
AirshipDock(AirshipDock),
Tavern(Tavern),
CoastalHouse(CoastalHouse),
CoastalWorkshop(CoastalWorkshop),
Workshop(Workshop),

View File

@ -0,0 +1,535 @@
use std::{mem::swap, ops::RangeInclusive};
use common::{
lottery::Lottery,
store::{Id, Store},
terrain::{Block, BlockKind},
};
use enum_map::EnumMap;
use fxhash::hash;
use rand::Rng;
use strum::{EnumIter, IntoEnumIterator};
use vek::*;
use crate::{
site2::{Dir, Fill, Site, Structure},
IndexRef, Land,
};
type Neighbor = Option<Id<Room>>;
struct Wall {
start: Vec2<i32>,
end: Vec2<i32>,
base_alt: i32,
top_alt: i32,
from: Neighbor,
to: Neighbor,
out_dir: Dir,
door: Option<i32>,
}
#[derive(Clone, Copy, EnumIter, enum_map::Enum)]
enum RoomKind {
Garden,
StageRoom,
BarRoom,
EntranceRoom,
}
impl RoomKind {
/// Returns the (side length size range, area size range)
fn size_range(&self) -> (RangeInclusive<i32>, RangeInclusive<i32>) {
match self {
RoomKind::Garden => (4..=20, 25..=250),
RoomKind::StageRoom => (10..=20, 130..=400),
RoomKind::BarRoom => (7..=14, 56..=196),
RoomKind::EntranceRoom => (3..=10, 9..=50),
}
}
}
struct Room {
/// Inclusive
bounds: Aabb<i32>,
kind: RoomKind,
// stairs: Option<Id<Stairs>>,
// walls: Vec<Wall>,
}
struct Stairs {
end: Vec2<i32>,
dir: Dir,
in_room: Id<Room>,
to_room: Id<Room>,
}
pub struct Tavern {
rooms: Store<Room>,
stairs: Store<Stairs>,
walls: Store<Wall>,
/// Tile position of the door tile
pub door_tile: Vec2<i32>,
pub(crate) door_wpos: Vec3<i32>,
/// Axis aligned bounding region for the house
bounds: Aabr<i32>,
}
impl Tavern {
pub fn generate(
land: &Land,
index: IndexRef,
rng: &mut impl Rng,
site: &Site,
door_tile: Vec2<i32>,
door_dir: Dir,
tile_aabr: Aabr<i32>,
) -> Self {
let mut rooms = Store::default();
let stairs = Store::default();
let mut walls = Store::default();
let mut room_counts = EnumMap::<RoomKind, u32>::default();
let bounds = Aabr {
min: site.tile_wpos(tile_aabr.min),
max: site.tile_wpos(tile_aabr.max),
};
let ibounds = Aabr {
min: bounds.min + 1,
max: bounds.max - 2,
};
let door_tile_center = site.tile_center_wpos(door_tile);
let door_wpos = door_dir.select_aabr_with(ibounds, door_tile_center);
let door_alt = land
.column_sample(door_wpos, index)
.map_or_else(|| land.get_alt_approx(door_wpos), |sample| sample.alt);
let door_wpos = door_wpos.with_z(door_alt.ceil() as i32);
/// Place room in bounds.
fn place_room_in(
room: RoomKind,
max_bounds: Aabr<i32>,
in_dir: Dir,
in_pos: Vec2<i32>,
rng: &mut impl Rng,
) -> Option<Aabr<i32>> {
let (size_range, area_range) = room.size_range();
let mut gen_range = |min, max, snap_max| {
let res = rng.gen_range(min..=max);
if snap_max <= max && snap_max - res <= 2 {
snap_max
} else {
res
}
};
let min = *size_range.start();
let snap_max = in_dir.select(max_bounds.size());
let max = snap_max.min(*size_range.end());
if max < min {
return None;
}
let size_x = gen_range(min, max, snap_max);
let min = ((*area_range.start() + size_x - 1) / size_x).max(*size_range.start());
let snap_max = in_dir.orthogonal().select(max_bounds.size());
let max = snap_max
.min(*size_range.end())
.min(*area_range.end() / size_x);
if max < min {
return None;
}
let size_y = gen_range(min, max, snap_max);
// calculate a valid aabr
let half_size_y = size_y / 2 + (size_y % 2) * rng.gen_range(0..=1);
let min = in_pos + in_dir.to_vec2() + in_dir.rotated_cw().to_vec2() * half_size_y;
let min = max_bounds.projected_point(min);
let max = min + in_dir.to_vec2() * size_x + in_dir.rotated_ccw().to_vec2() * size_y;
let max = max_bounds.projected_point(max);
let min = max - in_dir.to_vec2() * size_x + in_dir.rotated_cw().to_vec2() * size_y;
let bounds = Aabr { min, max }.made_valid();
Some(bounds)
}
struct RoomMeta {
id: Id<Room>,
walls: Vec<Dir>,
}
let mut room_metas = Vec::new();
{
let entrance_rooms =
Lottery::from(vec![(1.0, RoomKind::Garden), (2.0, RoomKind::EntranceRoom)]);
let entrance_room = *entrance_rooms.choose_seeded(rng.gen());
let entrance_room_hgt = rng.gen_range(3..=4);
let entrance_room_aabr =
place_room_in(entrance_room, ibounds, -door_dir, door_wpos.xy(), rng)
.expect("Not enough room in plot for a tavern");
let entrance_room_aabb = Aabb {
min: entrance_room_aabr.min.with_z(door_wpos.z),
max: entrance_room_aabr
.max
.with_z(door_wpos.z + entrance_room_hgt),
}
.made_valid();
let entrance_id = rooms.insert(Room {
bounds: entrance_room_aabb,
kind: entrance_room,
});
let start = door_dir.select_aabr_with(
entrance_room_aabr,
Vec2::broadcast(door_dir.rotated_cw().select_aabr(entrance_room_aabr)),
) + door_dir.rotated_cw().to_vec2()
+ door_dir.to_vec2();
walls.insert(Wall {
start,
end: door_dir.select_aabr_with(
entrance_room_aabr,
Vec2::broadcast(door_dir.rotated_ccw().select_aabr(entrance_room_aabr)),
) + door_dir.rotated_ccw().to_vec2()
+ door_dir.to_vec2(),
base_alt: entrance_room_aabb.min.z,
top_alt: entrance_room_aabb.max.z,
from: None,
to: Some(entrance_id),
out_dir: -door_dir,
door: Some(door_dir.rotated_cw().select(door_wpos.xy() - start).abs()),
});
room_metas.push(RoomMeta {
id: entrance_id,
walls: Dir::iter()
.filter(|d| *d != door_dir)
// .map(|d| {
// let a = d.rotated_cw().select_aabr(entrance_room_aabr);
// let b = d.rotated_ccw().select_aabr(entrance_room_aabr);
// (d, a.min(b)..=a.max(b))
// })
.collect(),
});
room_counts[entrance_room] += 1;
}
let to_aabr = |aabb: Aabb<i32>| Aabr {
min: aabb.min.xy(),
max: aabb.max.xy(),
};
'room_gen: while room_metas.len() > 0 {
let mut room_meta = room_metas.swap_remove(rng.gen_range(0..room_metas.len()));
if room_meta.walls.is_empty() {
continue 'room_gen;
}
let in_dir = room_meta
.walls
.swap_remove(rng.gen_range(0..room_meta.walls.len()));
let right = in_dir.orthogonal();
let left = -right;
let from_id = room_meta.id;
let from_room = &rooms[from_id];
if !room_meta.walls.is_empty() {
room_metas.push(room_meta);
}
let from_bounds = to_aabr(from_room.bounds);
// The maximum bounds, limited by the plot bounds and other rooms.
let mut max_bounds = Aabr {
min: in_dir.select_aabr_with(from_bounds, ibounds.min) + in_dir.to_vec2() * 2,
max: in_dir.select_aabr_with(ibounds, ibounds.max),
}
.made_valid();
// Take other rooms into account when calculating `max_bounds`. We don't care
// about this room if it's the originating room or at another
// height.
for (_, room) in rooms.iter().filter(|(room_id, room)| {
*room_id != from_id
&& room.bounds.min.z <= from_room.bounds.max.z
&& room.bounds.max.z >= from_room.bounds.min.z
}) {
let mut bounds = to_aabr(room.bounds);
bounds.min -= 2;
bounds.max += 2;
let intersection = bounds.intersection(max_bounds);
if intersection.is_valid() {
let Some(bounds) = Dir::iter()
.filter(|dir| {
*dir != in_dir
&& dir.select_aabr(intersection) * dir.signum()
< dir.select_aabr(max_bounds) * dir.signum()
})
.map(|min_dir| {
Aabr {
min: min_dir.select_aabr_with(
max_bounds,
Vec2::broadcast(min_dir.rotated_ccw().select_aabr(max_bounds)),
),
max: min_dir.select_aabr_with(
intersection,
Vec2::broadcast(min_dir.rotated_cw().select_aabr(max_bounds)),
),
}
.made_valid()
})
.filter(|bounds| {
left.select_aabr(*bounds) < right.select_aabr(from_bounds)
&& right.select_aabr(*bounds) > left.select_aabr(from_bounds)
})
.max_by_key(|bounds| bounds.size().product())
else {
continue 'room_gen;
};
max_bounds = bounds;
}
}
// the smallest side on the maximum bounds
let max_min_size = max_bounds.size().reduce_min();
// max bounds area
let max_area = max_bounds.size().product();
let room_lottery = RoomKind::iter()
// Filter out rooms that won't fit here.
.filter(|room_kind| {
let (size_range, area_range) = room_kind.size_range();
*size_range.start() <= max_min_size && *area_range.start() <= max_area
})
// Calculate chance for each room.
.map(|room_kind| {
(
match room_kind {
RoomKind::Garden => {
1.0 / (1.0 + room_counts[RoomKind::Garden] as f32 / 2.0)
},
RoomKind::StageRoom => {
2.0 / (1.0 + room_counts[RoomKind::StageRoom] as f32).powi(2)
},
RoomKind::BarRoom => {
2.0 / (1.0 + room_counts[RoomKind::BarRoom] as f32).powi(2)
},
RoomKind::EntranceRoom => {
0.1 / (1.0 + room_counts[RoomKind::EntranceRoom] as f32)
},
},
room_kind,
)
})
.collect::<Vec<_>>();
// We have no rooms to pick from.
if room_lottery.is_empty() {
continue 'room_gen;
}
// Pick a room.
let room_lottery = Lottery::from(room_lottery);
let room = *room_lottery.choose_seeded(rng.gen());
// Select a door position
let mut min = left
.select_aabr(from_bounds)
.max(left.select_aabr(max_bounds));
let mut max = right
.select_aabr(from_bounds)
.min(right.select_aabr(max_bounds));
if max < min {
swap(&mut min, &mut max);
}
if min + 2 > max {
continue 'room_gen;
}
let in_pos = rng.gen_range(min + 1..=max - 1);
let in_pos =
in_dir.select_aabr_with(from_bounds, Vec2::broadcast(in_pos)) + in_dir.to_vec2();
let Some(bounds) = place_room_in(room, max_bounds, in_dir, in_pos, rng) else {
continue 'room_gen;
};
let room_hgt = rng.gen_range(3..=5);
let bounds3 = Aabb {
min: bounds.min.with_z(from_room.bounds.min.z),
max: bounds.max.with_z(from_room.bounds.min.z + room_hgt),
};
let id = rooms.insert(Room {
bounds: bounds3,
kind: room,
});
let start = in_dir.select_aabr_with(
from_bounds,
Vec2::broadcast(left.select_aabr(from_bounds).max(left.select_aabr(bounds))),
) + in_dir.to_vec2()
+ left.to_vec2();
let end = in_dir.select_aabr_with(
from_bounds,
Vec2::broadcast(
right
.select_aabr(from_bounds)
.min(right.select_aabr(bounds)),
),
) + in_dir.to_vec2()
+ right.to_vec2();
walls.insert(Wall {
start,
end,
base_alt: bounds3.min.z,
top_alt: bounds3.max.z,
from: Some(from_id),
to: Some(id),
out_dir: in_dir,
door: Some(right.select(in_pos - start)),
});
room_metas.push(RoomMeta {
id,
walls: Dir::iter().filter(|d| *d != -in_dir).collect(),
});
}
Self {
rooms,
stairs,
walls,
door_tile,
door_wpos,
bounds,
}
}
}
fn aabb(mut aabb: Aabb<i32>) -> Aabb<i32> {
aabb.make_valid();
aabb.max += 1;
aabb
}
impl Structure for Tavern {
#[cfg(feature = "use-dyn-lib")]
const UPDATE_FN: &'static [u8] = b"render_tavern\0";
#[cfg_attr(feature = "be-dyn-lib", export_name = "render_tavern")]
fn render_inner(&self, _site: &Site, _land: &Land, painter: &crate::site2::Painter) {
let bounds = Aabr {
min: self.bounds.min,
max: self.bounds.max - 1,
};
let stone = Fill::Brick(BlockKind::Rock, Rgb::new(70, 70, 70), 10);
let wood = Fill::Block(Block::new(BlockKind::Wood, Rgb::new(106, 73, 64)));
painter
.aabb(aabb(Aabb {
min: bounds.min.with_z(self.door_wpos.z - 10),
max: bounds.max.with_z(self.door_wpos.z - 1),
}))
.fill(stone.clone());
for (id, room) in self.rooms.iter() {
painter.aabb(aabb(room.bounds)).clear();
let hash = hash(&id).to_le_bytes();
painter
.aabb(aabb(Aabb {
min: room.bounds.min.with_z(room.bounds.min.z - 1),
max: room.bounds.max.with_z(room.bounds.min.z - 1),
}))
.fill(Fill::Block(Block::new(
BlockKind::Wood,
Rgb::new(hash[0], hash[1], hash[2]),
)));
}
for (_, wall) in self.walls.iter() {
let get_kind = |room| self.rooms.get(room).kind;
let wall_aabb = Aabb {
min: wall.start.with_z(wall.base_alt),
max: wall.end.with_z(wall.top_alt),
};
match (wall.from.map(get_kind), wall.to.map(get_kind)) {
(Some(RoomKind::Garden), Some(RoomKind::Garden) | None)
| (None, Some(RoomKind::Garden)) => {
let hgt = wall_aabb.min.z..=wall_aabb.max.z;
painter
.column(wall_aabb.min.xy(), hgt.clone())
.fill(wood.clone());
painter.column(wall_aabb.max.xy(), hgt).fill(wood.clone());
painter
.aabb(aabb(Aabb {
min: wall_aabb.min,
max: wall_aabb.max.with_z(wall_aabb.min.z),
}))
.fill(wood.clone());
},
(None, None) => {},
_ => {
painter.aabb(aabb(wall_aabb)).fill(wood.clone());
},
}
let wall_dir = Dir::from_vector(wall.end - wall.start);
if let Some(door) = wall.door {
let door_pos = wall.start + wall_dir.to_vec2() * door;
let min = match wall.from {
None => door_pos - wall.out_dir.to_vec2(),
Some(_) => door_pos,
};
let max = match wall.to {
None => door_pos + wall.out_dir.to_vec2(),
Some(_) => door_pos,
};
painter
.aabb(aabb(Aabb {
min: min.with_z(wall.base_alt),
max: max.with_z(wall.base_alt + 2),
}))
.clear();
}
}
for (_, stairs) in self.stairs.iter() {
let down_room = &self.rooms[stairs.in_room];
let up_room = &self.rooms[stairs.to_room];
let down = -stairs.dir;
let right = stairs.dir.rotated_cw();
let aabr = Aabr {
min: stairs.end - right.to_vec2()
+ down.to_vec2() * (up_room.bounds.min.z - 1 - down_room.bounds.min.z),
max: stairs.end + right.to_vec2(),
};
painter
.aabb(aabb(Aabb {
min: aabr.min.with_z(up_room.bounds.min.z - 1),
max: aabr.max.with_z(up_room.bounds.min.z - 1),
}))
.clear();
painter
.ramp(
aabb(Aabb {
min: aabr.min.with_z(down_room.bounds.min.z),
max: aabr.max.with_z(up_room.bounds.min.z - 1),
}),
stairs.dir,
)
.fill(wood.clone());
}
}
}

View File

@ -6,7 +6,7 @@ use rand::Rng;
use vek::*;
/// A 2d direction.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, enum_map::Enum, strum::EnumIter)]
pub enum Dir {
X,
Y,