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 geom;
|
||||||
pub mod ids;
|
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 geom::{Dir, Grid, Pos, Rect, Size};
|
||||||
pub use ids::*;
|
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