feat(citygen): tile, world and net data model
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,14 @@
|
||||
|
||||
pub mod geom;
|
||||
pub mod ids;
|
||||
pub mod net;
|
||||
pub mod params;
|
||||
pub mod tile;
|
||||
pub mod world;
|
||||
|
||||
pub use geom::{Dir, Grid, Pos, Rect, Size};
|
||||
pub use ids::*;
|
||||
pub use net::*;
|
||||
pub use params::*;
|
||||
pub use tile::*;
|
||||
pub use world::*;
|
||||
|
||||
89
crates/citygen/src/net.rs
Normal file
89
crates/citygen/src/net.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
//! The Net: subnets, nodes and links anchored to physical tiles.
|
||||
|
||||
use crate::geom::Pos;
|
||||
use crate::ids::{BuildingId, DistrictId, NodeId, SubnetId};
|
||||
use crate::world::StratumKind;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Net {
|
||||
pub subnets: Vec<Subnet>,
|
||||
pub nodes: Vec<Node>,
|
||||
pub links: Vec<Link>,
|
||||
}
|
||||
|
||||
/// Undirected link between two nodes.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct Link(pub NodeId, pub NodeId);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum SubnetOwner {
|
||||
Building(BuildingId),
|
||||
District(DistrictId),
|
||||
City,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Subnet {
|
||||
pub id: SubnetId,
|
||||
pub owner: SubnetOwner,
|
||||
pub security: u8,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum NodeKind {
|
||||
Terminal,
|
||||
Camera,
|
||||
DoorLock,
|
||||
LiftCtrl,
|
||||
AdBoard,
|
||||
LampGrid,
|
||||
Server,
|
||||
DataVault,
|
||||
Ice,
|
||||
Backbone,
|
||||
}
|
||||
|
||||
impl NodeKind {
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
NodeKind::Terminal => "terminal",
|
||||
NodeKind::Camera => "camera",
|
||||
NodeKind::DoorLock => "door lock",
|
||||
NodeKind::LiftCtrl => "lift control",
|
||||
NodeKind::AdBoard => "ad board",
|
||||
NodeKind::LampGrid => "lamp grid",
|
||||
NodeKind::Server => "server",
|
||||
NodeKind::DataVault => "data vault",
|
||||
NodeKind::Ice => "ICE",
|
||||
NodeKind::Backbone => "backbone",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Node {
|
||||
pub id: NodeId,
|
||||
pub kind: NodeKind,
|
||||
pub anchor: (StratumKind, Pos),
|
||||
pub subnet: SubnetId,
|
||||
pub building: Option<BuildingId>,
|
||||
}
|
||||
|
||||
impl Net {
|
||||
pub fn nodes_on(&self, k: StratumKind) -> impl Iterator<Item = &Node> {
|
||||
self.nodes.iter().filter(move |n| n.anchor.0 == k)
|
||||
}
|
||||
|
||||
pub fn node_at(&self, k: StratumKind, p: Pos) -> Option<&Node> {
|
||||
self.nodes.iter().find(|n| n.anchor == (k, p))
|
||||
}
|
||||
|
||||
pub fn subnet(&self, id: SubnetId) -> &Subnet {
|
||||
&self.subnets[id.index()]
|
||||
}
|
||||
|
||||
pub fn node(&self, id: NodeId) -> &Node {
|
||||
&self.nodes[id.index()]
|
||||
}
|
||||
}
|
||||
76
crates/citygen/src/params.rs
Normal file
76
crates/citygen/src/params.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
//! Generation parameters.
|
||||
|
||||
use crate::geom::Size;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum SizePreset {
|
||||
Small,
|
||||
Medium,
|
||||
Large,
|
||||
Custom { w: u32, h: u32 },
|
||||
}
|
||||
|
||||
impl SizePreset {
|
||||
pub fn size(self) -> Size {
|
||||
match self {
|
||||
SizePreset::Small => Size { w: 160, h: 80 },
|
||||
SizePreset::Medium => Size { w: 240, h: 120 },
|
||||
SizePreset::Large => Size { w: 320, h: 160 },
|
||||
SizePreset::Custom { w, h } => Size { w, h },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
SizePreset::Small => "small",
|
||||
SizePreset::Medium => "medium",
|
||||
SizePreset::Large => "large",
|
||||
SizePreset::Custom { .. } => "custom",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum WaterMode {
|
||||
None,
|
||||
Bay,
|
||||
River,
|
||||
}
|
||||
|
||||
impl WaterMode {
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
WaterMode::None => "none",
|
||||
WaterMode::Bay => "bay",
|
||||
WaterMode::River => "river",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GenParams {
|
||||
pub seed: u64,
|
||||
pub size: SizePreset,
|
||||
pub districts: u8,
|
||||
pub density: f32,
|
||||
pub wealth_skew: f32,
|
||||
pub decay: f32,
|
||||
pub water: WaterMode,
|
||||
pub flavor: String,
|
||||
}
|
||||
|
||||
impl Default for GenParams {
|
||||
fn default() -> Self {
|
||||
GenParams {
|
||||
seed: 0,
|
||||
size: SizePreset::Medium,
|
||||
districts: 7,
|
||||
density: 0.6,
|
||||
wealth_skew: 0.0,
|
||||
decay: 0.4,
|
||||
water: WaterMode::Bay,
|
||||
flavor: "default".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
216
crates/citygen/src/tile.rs
Normal file
216
crates/citygen/src/tile.rs
Normal file
@@ -0,0 +1,216 @@
|
||||
//! Tiles: terrain, features and derived walkability.
|
||||
|
||||
use crate::ids::{BuildingId, DistrictId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Base terrain of a tile. Every tile has exactly one.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum Terrain {
|
||||
Void,
|
||||
Water,
|
||||
Sewage,
|
||||
Road,
|
||||
Sidewalk,
|
||||
Alley,
|
||||
Plaza,
|
||||
Floor,
|
||||
Wall,
|
||||
Door,
|
||||
Window,
|
||||
Rooftop,
|
||||
Walkway,
|
||||
Bridge,
|
||||
Tunnel,
|
||||
Rail,
|
||||
Stair,
|
||||
Lift,
|
||||
Ladder,
|
||||
Grate,
|
||||
}
|
||||
|
||||
pub const ALL_TERRAIN: [Terrain; 20] = [
|
||||
Terrain::Void,
|
||||
Terrain::Water,
|
||||
Terrain::Sewage,
|
||||
Terrain::Road,
|
||||
Terrain::Sidewalk,
|
||||
Terrain::Alley,
|
||||
Terrain::Plaza,
|
||||
Terrain::Floor,
|
||||
Terrain::Wall,
|
||||
Terrain::Door,
|
||||
Terrain::Window,
|
||||
Terrain::Rooftop,
|
||||
Terrain::Walkway,
|
||||
Terrain::Bridge,
|
||||
Terrain::Tunnel,
|
||||
Terrain::Rail,
|
||||
Terrain::Stair,
|
||||
Terrain::Lift,
|
||||
Terrain::Ladder,
|
||||
Terrain::Grate,
|
||||
];
|
||||
|
||||
/// Named neon colours; the renderer maps them to RGB.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum Hue {
|
||||
Cyan,
|
||||
Magenta,
|
||||
Pink,
|
||||
Orange,
|
||||
Yellow,
|
||||
Green,
|
||||
Blue,
|
||||
Red,
|
||||
White,
|
||||
}
|
||||
|
||||
/// Optional dressing on top of the terrain.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum Feature {
|
||||
Neon(Hue),
|
||||
HoloAd,
|
||||
Lamp,
|
||||
Vent,
|
||||
Puddle,
|
||||
Trash,
|
||||
Barrier,
|
||||
Stall,
|
||||
Sign(char),
|
||||
Terminal,
|
||||
Camera,
|
||||
AcUnit,
|
||||
Helipad,
|
||||
Pipe,
|
||||
Cable,
|
||||
}
|
||||
|
||||
pub const ALL_FEATURES: [Feature; 15] = [
|
||||
Feature::Neon(Hue::Cyan),
|
||||
Feature::HoloAd,
|
||||
Feature::Lamp,
|
||||
Feature::Vent,
|
||||
Feature::Puddle,
|
||||
Feature::Trash,
|
||||
Feature::Barrier,
|
||||
Feature::Stall,
|
||||
Feature::Sign('ア'),
|
||||
Feature::Terminal,
|
||||
Feature::Camera,
|
||||
Feature::AcUnit,
|
||||
Feature::Helipad,
|
||||
Feature::Pipe,
|
||||
Feature::Cable,
|
||||
];
|
||||
|
||||
/// One cell of a stratum.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Tile {
|
||||
pub terrain: Terrain,
|
||||
pub feature: Option<Feature>,
|
||||
pub district: Option<DistrictId>,
|
||||
pub building: Option<BuildingId>,
|
||||
/// Deterministic per-tile hash used for glyph variation and animation phase.
|
||||
pub variant: u8,
|
||||
}
|
||||
|
||||
impl Tile {
|
||||
pub const VOID: Tile = Tile {
|
||||
terrain: Terrain::Void,
|
||||
feature: None,
|
||||
district: None,
|
||||
building: None,
|
||||
variant: 0,
|
||||
};
|
||||
|
||||
pub fn new(terrain: Terrain) -> Self {
|
||||
Tile {
|
||||
terrain,
|
||||
..Tile::VOID
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_walkable(&self) -> bool {
|
||||
use Terrain::*;
|
||||
let terrain_ok = matches!(
|
||||
self.terrain,
|
||||
Road | Sidewalk
|
||||
| Alley
|
||||
| Plaza
|
||||
| Floor
|
||||
| Door
|
||||
| Rooftop
|
||||
| Walkway
|
||||
| Bridge
|
||||
| Tunnel
|
||||
| Rail
|
||||
| Stair
|
||||
| Lift
|
||||
| Ladder
|
||||
| Grate
|
||||
);
|
||||
let feature_ok = !matches!(
|
||||
self.feature,
|
||||
Some(
|
||||
Feature::Barrier
|
||||
| Feature::Stall
|
||||
| Feature::AcUnit
|
||||
| Feature::Pipe
|
||||
| Feature::Terminal
|
||||
)
|
||||
);
|
||||
terrain_ok && feature_ok
|
||||
}
|
||||
|
||||
pub fn blocks_sight(&self) -> bool {
|
||||
matches!(self.terrain, Terrain::Wall | Terrain::Door)
|
||||
}
|
||||
|
||||
/// Outdoor ground at street level: roads, sidewalks, alleys, plazas.
|
||||
pub fn is_outdoor_ground(&self) -> bool {
|
||||
matches!(
|
||||
self.terrain,
|
||||
Terrain::Road | Terrain::Sidewalk | Terrain::Alley | Terrain::Plaza
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_building_part(&self) -> bool {
|
||||
matches!(
|
||||
self.terrain,
|
||||
Terrain::Wall | Terrain::Floor | Terrain::Door | Terrain::Window
|
||||
)
|
||||
}
|
||||
|
||||
/// A vertical connector tile.
|
||||
pub fn is_connector(&self) -> bool {
|
||||
matches!(
|
||||
self.terrain,
|
||||
Terrain::Stair | Terrain::Lift | Terrain::Ladder
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn walkability_rules() {
|
||||
assert!(Tile::new(Terrain::Sidewalk).is_walkable());
|
||||
assert!(!Tile::new(Terrain::Wall).is_walkable());
|
||||
assert!(!Tile::new(Terrain::Water).is_walkable());
|
||||
let mut t = Tile::new(Terrain::Road);
|
||||
t.feature = Some(Feature::Barrier);
|
||||
assert!(!t.is_walkable());
|
||||
t.feature = Some(Feature::Lamp);
|
||||
assert!(t.is_walkable());
|
||||
assert!(Tile::new(Terrain::Wall).blocks_sight());
|
||||
assert!(Tile::new(Terrain::Door).blocks_sight());
|
||||
assert!(!Tile::new(Terrain::Window).blocks_sight());
|
||||
assert!(!Tile::new(Terrain::Window).is_walkable());
|
||||
assert!(Tile::new(Terrain::Plaza).is_outdoor_ground());
|
||||
assert!(Tile::new(Terrain::Door).is_building_part());
|
||||
assert_eq!(ALL_TERRAIN.len(), 20);
|
||||
assert_eq!(ALL_FEATURES.len(), 15);
|
||||
}
|
||||
}
|
||||
439
crates/citygen/src/world.rs
Normal file
439
crates/citygen/src/world.rs
Normal file
@@ -0,0 +1,439 @@
|
||||
//! The city: strata, districts, buildings, connectors.
|
||||
|
||||
use crate::geom::{Grid, Pos, Rect, Size};
|
||||
use crate::ids::{BuildingId, DistrictId};
|
||||
use crate::net::Net;
|
||||
use crate::params::GenParams;
|
||||
use crate::tile::Tile;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A vertical level of the city. `Net` is reserved for the cyberspace projection.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub enum StratumKind {
|
||||
Undercity,
|
||||
Street,
|
||||
Skyways,
|
||||
Heights,
|
||||
Net,
|
||||
}
|
||||
|
||||
impl StratumKind {
|
||||
pub const PHYSICAL: [StratumKind; 4] = [
|
||||
StratumKind::Undercity,
|
||||
StratumKind::Street,
|
||||
StratumKind::Skyways,
|
||||
StratumKind::Heights,
|
||||
];
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
StratumKind::Undercity => "Undercity",
|
||||
StratumKind::Street => "Street",
|
||||
StratumKind::Skyways => "Skyways",
|
||||
StratumKind::Heights => "Heights",
|
||||
StratumKind::Net => "Net",
|
||||
}
|
||||
}
|
||||
|
||||
/// Position in the physical stack, bottom first.
|
||||
pub fn index(self) -> usize {
|
||||
match self {
|
||||
StratumKind::Undercity => 0,
|
||||
StratumKind::Street => 1,
|
||||
StratumKind::Skyways => 2,
|
||||
StratumKind::Heights => 3,
|
||||
StratumKind::Net => 4,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_index(i: usize) -> Option<StratumKind> {
|
||||
match i {
|
||||
0 => Some(StratumKind::Undercity),
|
||||
1 => Some(StratumKind::Street),
|
||||
2 => Some(StratumKind::Skyways),
|
||||
3 => Some(StratumKind::Heights),
|
||||
4 => Some(StratumKind::Net),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn above(self) -> Option<StratumKind> {
|
||||
match self {
|
||||
StratumKind::Undercity => Some(StratumKind::Street),
|
||||
StratumKind::Street => Some(StratumKind::Skyways),
|
||||
StratumKind::Skyways => Some(StratumKind::Heights),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn below(self) -> Option<StratumKind> {
|
||||
match self {
|
||||
StratumKind::Street => Some(StratumKind::Undercity),
|
||||
StratumKind::Skyways => Some(StratumKind::Street),
|
||||
StratumKind::Heights => Some(StratumKind::Skyways),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One full tile map plus its precomputed light.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Stratum {
|
||||
pub kind: StratumKind,
|
||||
pub tiles: Grid<Tile>,
|
||||
/// 0 dark .. 255 bright.
|
||||
pub light: Grid<u8>,
|
||||
}
|
||||
|
||||
impl Stratum {
|
||||
pub fn new(kind: StratumKind, size: Size) -> Self {
|
||||
Stratum {
|
||||
kind,
|
||||
tiles: Grid::filled(size, Tile::VOID),
|
||||
light: Grid::filled(size, 0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum Theme {
|
||||
CorpCore,
|
||||
NightMarket,
|
||||
Megablocks,
|
||||
CombatZone,
|
||||
Docks,
|
||||
Industrial,
|
||||
NeonStrip,
|
||||
OldTown,
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
pub const ALL: [Theme; 8] = [
|
||||
Theme::CorpCore,
|
||||
Theme::NightMarket,
|
||||
Theme::Megablocks,
|
||||
Theme::CombatZone,
|
||||
Theme::Docks,
|
||||
Theme::Industrial,
|
||||
Theme::NeonStrip,
|
||||
Theme::OldTown,
|
||||
];
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Theme::CorpCore => "Corp Core",
|
||||
Theme::NightMarket => "Night Market",
|
||||
Theme::Megablocks => "Megablocks",
|
||||
Theme::CombatZone => "Combat Zone",
|
||||
Theme::Docks => "Docks",
|
||||
Theme::Industrial => "Industrial",
|
||||
Theme::NeonStrip => "Neon Strip",
|
||||
Theme::OldTown => "Old Town",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct District {
|
||||
pub id: DistrictId,
|
||||
pub name: String,
|
||||
pub theme: Theme,
|
||||
pub center: Pos,
|
||||
pub wealth: f32,
|
||||
pub decay: f32,
|
||||
pub bounds: Rect,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum BuildingKind {
|
||||
CorpTower,
|
||||
Office,
|
||||
Megablock,
|
||||
Apartment,
|
||||
Hotel,
|
||||
Shop,
|
||||
NoodleBar,
|
||||
Bar,
|
||||
Club,
|
||||
Arcade,
|
||||
Pawn,
|
||||
Clinic,
|
||||
Temple,
|
||||
Warehouse,
|
||||
Factory,
|
||||
Depot,
|
||||
Garage,
|
||||
Parking,
|
||||
Market,
|
||||
}
|
||||
|
||||
impl BuildingKind {
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
BuildingKind::CorpTower => "corp tower",
|
||||
BuildingKind::Office => "office",
|
||||
BuildingKind::Megablock => "megablock",
|
||||
BuildingKind::Apartment => "apartment",
|
||||
BuildingKind::Hotel => "hotel",
|
||||
BuildingKind::Shop => "shop",
|
||||
BuildingKind::NoodleBar => "noodle bar",
|
||||
BuildingKind::Bar => "bar",
|
||||
BuildingKind::Club => "club",
|
||||
BuildingKind::Arcade => "arcade",
|
||||
BuildingKind::Pawn => "pawn shop",
|
||||
BuildingKind::Clinic => "clinic",
|
||||
BuildingKind::Temple => "temple",
|
||||
BuildingKind::Warehouse => "warehouse",
|
||||
BuildingKind::Factory => "factory",
|
||||
BuildingKind::Depot => "depot",
|
||||
BuildingKind::Garage => "garage",
|
||||
BuildingKind::Parking => "parking",
|
||||
BuildingKind::Market => "market",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_corp(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
BuildingKind::CorpTower | BuildingKind::Office | BuildingKind::Megablock
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum HeightClass {
|
||||
Low,
|
||||
Mid,
|
||||
Tall,
|
||||
}
|
||||
|
||||
/// Inclusive range of physical strata a building occupies.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct StrataSpan {
|
||||
pub lo: StratumKind,
|
||||
pub hi: StratumKind,
|
||||
}
|
||||
|
||||
impl StrataSpan {
|
||||
pub fn from_class(class: HeightClass, basement: bool) -> Self {
|
||||
let hi = match class {
|
||||
HeightClass::Low => StratumKind::Street,
|
||||
HeightClass::Mid => StratumKind::Skyways,
|
||||
HeightClass::Tall => StratumKind::Heights,
|
||||
};
|
||||
let lo = if basement {
|
||||
StratumKind::Undercity
|
||||
} else {
|
||||
StratumKind::Street
|
||||
};
|
||||
StrataSpan { lo, hi }
|
||||
}
|
||||
|
||||
pub fn contains(self, k: StratumKind) -> bool {
|
||||
k.index() >= self.lo.index() && k.index() <= self.hi.index()
|
||||
}
|
||||
|
||||
pub fn iter(self) -> impl Iterator<Item = StratumKind> {
|
||||
(self.lo.index()..=self.hi.index()).filter_map(StratumKind::from_index)
|
||||
}
|
||||
|
||||
pub fn len(self) -> usize {
|
||||
self.hi.index() - self.lo.index() + 1
|
||||
}
|
||||
|
||||
pub fn is_empty(self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Building {
|
||||
pub id: BuildingId,
|
||||
pub kind: BuildingKind,
|
||||
pub district: DistrictId,
|
||||
/// Footprint at Street level; the same rect on every stratum of `span`.
|
||||
pub footprint: Rect,
|
||||
pub span: StrataSpan,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum ConnectorKind {
|
||||
Stair,
|
||||
Lift,
|
||||
Ladder,
|
||||
Manhole,
|
||||
Jack,
|
||||
}
|
||||
|
||||
impl ConnectorKind {
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
ConnectorKind::Stair => "stairs",
|
||||
ConnectorKind::Lift => "lift",
|
||||
ConnectorKind::Ladder => "ladder",
|
||||
ConnectorKind::Manhole => "manhole",
|
||||
ConnectorKind::Jack => "jack",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A vertical link between strata at one position.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Connector {
|
||||
pub kind: ConnectorKind,
|
||||
pub pos: Pos,
|
||||
/// Served strata, ordered low to high.
|
||||
pub serves: Vec<StratumKind>,
|
||||
/// Reachable from the street without entering a building.
|
||||
pub public: bool,
|
||||
}
|
||||
|
||||
impl Connector {
|
||||
/// The strata directly below and above `from` in `serves`, or `None` if not served.
|
||||
pub fn serves_from(
|
||||
&self,
|
||||
from: StratumKind,
|
||||
) -> Option<(Option<StratumKind>, Option<StratumKind>)> {
|
||||
let i = self.serves.iter().position(|k| *k == from)?;
|
||||
let below = if i > 0 {
|
||||
Some(self.serves[i - 1])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let above = self.serves.get(i + 1).copied();
|
||||
Some((below, above))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct City {
|
||||
pub seed: u64,
|
||||
pub params: GenParams,
|
||||
pub name: String,
|
||||
pub size: Size,
|
||||
pub strata: Vec<Stratum>,
|
||||
pub districts: Vec<District>,
|
||||
pub buildings: Vec<Building>,
|
||||
pub connectors: Vec<Connector>,
|
||||
/// Centre paths of the arterial roads, in generation order.
|
||||
pub arterials: Vec<Vec<Pos>>,
|
||||
pub net: Net,
|
||||
}
|
||||
|
||||
impl City {
|
||||
/// Four physical strata of `Void`, nothing else.
|
||||
pub fn empty(params: GenParams) -> City {
|
||||
let size = params.size.size();
|
||||
City {
|
||||
seed: params.seed,
|
||||
name: String::new(),
|
||||
size,
|
||||
strata: StratumKind::PHYSICAL
|
||||
.iter()
|
||||
.map(|k| Stratum::new(*k, size))
|
||||
.collect(),
|
||||
districts: Vec::new(),
|
||||
buildings: Vec::new(),
|
||||
connectors: Vec::new(),
|
||||
arterials: Vec::new(),
|
||||
net: Net::default(),
|
||||
params,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stratum(&self, k: StratumKind) -> Option<&Stratum> {
|
||||
self.strata.iter().find(|s| s.kind == k)
|
||||
}
|
||||
|
||||
pub fn stratum_mut(&mut self, k: StratumKind) -> Option<&mut Stratum> {
|
||||
self.strata.iter_mut().find(|s| s.kind == k)
|
||||
}
|
||||
|
||||
pub fn tile(&self, k: StratumKind, p: Pos) -> Option<&Tile> {
|
||||
self.stratum(k).and_then(|s| s.tiles.get(p))
|
||||
}
|
||||
|
||||
pub fn tile_mut(&mut self, k: StratumKind, p: Pos) -> Option<&mut Tile> {
|
||||
self.stratum_mut(k).and_then(|s| s.tiles.get_mut(p))
|
||||
}
|
||||
|
||||
pub fn walkable(&self, k: StratumKind, p: Pos) -> bool {
|
||||
self.tile(k, p).is_some_and(|t| t.is_walkable())
|
||||
}
|
||||
|
||||
pub fn connectors_at(&self, p: Pos) -> impl Iterator<Item = &Connector> {
|
||||
self.connectors.iter().filter(move |c| c.pos == p)
|
||||
}
|
||||
|
||||
pub fn district(&self, id: DistrictId) -> &District {
|
||||
&self.districts[id.index()]
|
||||
}
|
||||
|
||||
pub fn building(&self, id: BuildingId) -> &Building {
|
||||
&self.buildings[id.index()]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::params::{GenParams, SizePreset};
|
||||
use crate::tile::Terrain;
|
||||
|
||||
#[test]
|
||||
fn span_iter_and_class() {
|
||||
let s = StrataSpan::from_class(HeightClass::Tall, true);
|
||||
assert_eq!(s.iter().collect::<Vec<_>>(), StratumKind::PHYSICAL.to_vec());
|
||||
let s = StrataSpan::from_class(HeightClass::Mid, false);
|
||||
assert_eq!(
|
||||
s.iter().collect::<Vec<_>>(),
|
||||
vec![StratumKind::Street, StratumKind::Skyways]
|
||||
);
|
||||
assert!(s.contains(StratumKind::Skyways));
|
||||
assert!(!s.contains(StratumKind::Heights));
|
||||
assert_eq!(s.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn city_empty_has_four_void_strata() {
|
||||
let c = City::empty(GenParams {
|
||||
size: SizePreset::Custom { w: 10, h: 5 },
|
||||
..GenParams::default()
|
||||
});
|
||||
assert_eq!(c.strata.len(), 4);
|
||||
assert_eq!(
|
||||
c.stratum(StratumKind::Heights).unwrap().tiles.size(),
|
||||
Size { w: 10, h: 5 }
|
||||
);
|
||||
assert_eq!(
|
||||
c.tile(StratumKind::Street, Pos::new(3, 3)).unwrap().terrain,
|
||||
Terrain::Void
|
||||
);
|
||||
assert!(c.tile(StratumKind::Street, Pos::new(10, 3)).is_none());
|
||||
assert!(!c.walkable(StratumKind::Street, Pos::new(3, 3)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connector_neighbors() {
|
||||
let c = Connector {
|
||||
kind: ConnectorKind::Lift,
|
||||
pos: Pos::new(0, 0),
|
||||
serves: vec![
|
||||
StratumKind::Street,
|
||||
StratumKind::Skyways,
|
||||
StratumKind::Heights,
|
||||
],
|
||||
public: false,
|
||||
};
|
||||
assert_eq!(
|
||||
c.serves_from(StratumKind::Skyways),
|
||||
Some((Some(StratumKind::Street), Some(StratumKind::Heights)))
|
||||
);
|
||||
assert_eq!(
|
||||
c.serves_from(StratumKind::Heights),
|
||||
Some((Some(StratumKind::Skyways), None))
|
||||
);
|
||||
assert_eq!(c.serves_from(StratumKind::Undercity), None);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user