docs: README, dead-code cleanup, smoke fixes
- README with run instructions and key reference - lift the crate-level dead_code allowance; scope it to the anim library surface - render paths tolerate a city that is still generating (district/building ids without entries), found by driving the app through a pty - status messages show in the status bar, not only inside the inspector - buffered JSON export (26 s -> 0.4 s for a large city) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
//! Tick-driven animation primitives: easing, tweens, timelines.
|
||||
//!
|
||||
//! This is a small library surface for every screen, so parts of it (extra easings,
|
||||
//! `Timeline`) are exercised by tests before any screen uses them.
|
||||
#![allow(dead_code)]
|
||||
|
||||
pub mod effects;
|
||||
|
||||
|
||||
@@ -109,7 +109,9 @@ pub fn run() -> Result<()> {
|
||||
if json {
|
||||
let file = std::fs::File::create(&out)
|
||||
.with_context(|| format!("creating {}", out.display()))?;
|
||||
serde_json::to_writer_pretty(file, &city)?;
|
||||
let mut writer = std::io::BufWriter::new(file);
|
||||
serde_json::to_writer_pretty(&mut writer, &city)?;
|
||||
std::io::Write::flush(&mut writer)?;
|
||||
} else {
|
||||
save_city(&out, &city)?;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
//! `city`: generate, explore and walk a cyberpunk city in the terminal.
|
||||
|
||||
// Screens, widgets and overlays land task by task; lifted once the app is wired up.
|
||||
#![allow(dead_code)]
|
||||
|
||||
mod anim;
|
||||
mod app;
|
||||
mod cli;
|
||||
|
||||
@@ -56,7 +56,10 @@ impl Widget for Minimap<'_> {
|
||||
water += 1;
|
||||
}
|
||||
if theme.is_none() {
|
||||
theme = t.district.map(|d| self.city.district(d).theme);
|
||||
theme = t
|
||||
.district
|
||||
.and_then(|d| self.city.district_opt(d))
|
||||
.map(|d| d.theme);
|
||||
}
|
||||
}
|
||||
let cell = &mut buf[(area.x + cx, area.y + cy)];
|
||||
|
||||
@@ -9,7 +9,7 @@ pub mod traffic;
|
||||
pub mod viewport;
|
||||
pub mod weather;
|
||||
|
||||
use citygen::{BuildingKind, ConnectorKind, Pos, StratumKind, Theme};
|
||||
use citygen::{BuildingKind, ConnectorKind, StratumKind, Theme};
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -95,7 +95,6 @@ pub struct RenderCtx<'a> {
|
||||
pub stratum: StratumKind,
|
||||
pub light: u8,
|
||||
pub tick: u64,
|
||||
pub pos: Pos,
|
||||
pub vis: Visibility,
|
||||
pub connector: Option<ConnDir>,
|
||||
pub kind: Option<BuildingKind>,
|
||||
|
||||
@@ -337,9 +337,7 @@ mod tests {
|
||||
use crate::render::lighting::apply_vis;
|
||||
use crate::render::style::StylePack;
|
||||
use crate::render::{RenderCtx, Visibility};
|
||||
use citygen::{
|
||||
ALL_FEATURES, ALL_TERRAIN, Feature, Hue, Pos, StratumKind, Terrain, Theme, Tile,
|
||||
};
|
||||
use citygen::{ALL_FEATURES, ALL_TERRAIN, Feature, Hue, StratumKind, Terrain, Theme, Tile};
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
#[test]
|
||||
@@ -389,7 +387,6 @@ mod tests {
|
||||
stratum: StratumKind::Street,
|
||||
light,
|
||||
tick,
|
||||
pos: Pos::new(1, 1),
|
||||
vis: Visibility::Visible,
|
||||
connector: None,
|
||||
kind: None,
|
||||
|
||||
@@ -188,14 +188,19 @@ impl Widget for MapView<'_> {
|
||||
.unwrap_or(Visibility::Visible);
|
||||
let ctx = RenderCtx {
|
||||
style: self.style,
|
||||
theme: tile.district.map(|d| self.city.district(d).theme),
|
||||
theme: tile
|
||||
.district
|
||||
.and_then(|d| self.city.district_opt(d))
|
||||
.map(|d| d.theme),
|
||||
stratum: self.stratum,
|
||||
light: st.light.get(p).copied().unwrap_or(0),
|
||||
tick: self.tick,
|
||||
pos: p,
|
||||
vis,
|
||||
connector,
|
||||
kind: tile.building.map(|b| self.city.building(b).kind),
|
||||
kind: tile
|
||||
.building
|
||||
.and_then(|b| self.city.building_opt(b))
|
||||
.map(|b| b.kind),
|
||||
wall_mask: mask,
|
||||
};
|
||||
apply_vis(cell(tile, &ctx), vis).write(buf, sx, sy);
|
||||
@@ -282,6 +287,46 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapview_renders_every_partial_generation_state() {
|
||||
use citygen::{Generator, Progress};
|
||||
let s = StylePack::load().unwrap();
|
||||
let mut g = Generator::new(GenParams {
|
||||
seed: 11,
|
||||
size: SizePreset::Custom { w: 80, h: 40 },
|
||||
districts: 4,
|
||||
..Default::default()
|
||||
})
|
||||
.unwrap();
|
||||
let mut t = Terminal::new(TestBackend::new(90, 45)).unwrap();
|
||||
let mut steps = 0u32;
|
||||
loop {
|
||||
let progress = g.step();
|
||||
steps += 1;
|
||||
if steps.is_multiple_of(7) || matches!(progress, Progress::Done) {
|
||||
for k in StratumKind::PHYSICAL {
|
||||
t.draw(|f| {
|
||||
MapView {
|
||||
city: g.city(),
|
||||
stratum: k,
|
||||
origin: Pos::new(-2, -2),
|
||||
tick: steps as u64,
|
||||
style: &s,
|
||||
vis: None,
|
||||
overlays: &[&Weather],
|
||||
cursor: None,
|
||||
}
|
||||
.render(f.area(), f.buffer_mut())
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
if matches!(progress, Progress::Done) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn camera_follow_has_deadzone_and_eases() {
|
||||
let mut cam = Camera::default();
|
||||
|
||||
@@ -417,13 +417,6 @@ impl Screen for MapScreen {
|
||||
let node = focus.map(|id| city.net.node(id));
|
||||
lines.extend(inspector_lines(city, self.stratum, cursor, node));
|
||||
}
|
||||
if let Some(msg) = st.status_text() {
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::styled(
|
||||
format!(" {msg}"),
|
||||
Style::default().fg(Color::Rgb(255, 200, 80)),
|
||||
));
|
||||
}
|
||||
f.render_widget(
|
||||
Paragraph::new(lines).wrap(ratatui::widgets::Wrap { trim: false }),
|
||||
inner,
|
||||
@@ -435,7 +428,13 @@ impl Screen for MapScreen {
|
||||
Mode::Walk => "hjkl move · <> stairs · tab camera · d net · ? keys",
|
||||
};
|
||||
f.render_widget(
|
||||
Paragraph::new(status_bar(Some(city), self.stratum, self.fps.fps, hints)),
|
||||
Paragraph::new(status_bar(
|
||||
Some(city),
|
||||
self.stratum,
|
||||
self.fps.fps,
|
||||
hints,
|
||||
st.status_text(),
|
||||
)),
|
||||
status,
|
||||
);
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ impl Generating {
|
||||
}
|
||||
|
||||
/// Something that changes whenever generation advances; used by tests.
|
||||
#[cfg(test)]
|
||||
pub fn progress_marker(&self) -> (usize, Option<(Stage, u32)>) {
|
||||
(
|
||||
self.done.len(),
|
||||
|
||||
@@ -38,13 +38,6 @@ impl Menu {
|
||||
pub fn entrance(&self, i: usize, tick: u64) -> f32 {
|
||||
Tween::new(self.opened + i as u64 * 4, 8, Easing::OutCubic).progress(tick)
|
||||
}
|
||||
|
||||
pub fn settled(&self, tick: u64) -> bool {
|
||||
self.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.all(|(i, _)| self.entrance(i, tick) >= 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MenuView<'a> {
|
||||
|
||||
@@ -46,8 +46,7 @@ pub fn inspector_lines(
|
||||
if let Some(f) = t.feature {
|
||||
out.push(kv("feature", format!("{f:?}").to_lowercase()));
|
||||
}
|
||||
if let Some(d) = t.district {
|
||||
let d = city.district(d);
|
||||
if let Some(d) = t.district.and_then(|d| city.district_opt(d)) {
|
||||
out.push(kv("district", d.name.clone()));
|
||||
out.push(kv("theme", d.theme.label()));
|
||||
out.push(kv(
|
||||
@@ -55,8 +54,7 @@ pub fn inspector_lines(
|
||||
format!("{:.0}% decay {:.0}%", d.wealth * 100.0, d.decay * 100.0),
|
||||
));
|
||||
}
|
||||
if let Some(b) = t.building {
|
||||
let b = city.building(b);
|
||||
if let Some(b) = t.building.and_then(|b| city.building_opt(b)) {
|
||||
out.push(kv(
|
||||
"building",
|
||||
b.name.clone().unwrap_or_else(|| b.kind.label().to_string()),
|
||||
@@ -149,6 +147,7 @@ pub fn status_bar(
|
||||
stratum: StratumKind,
|
||||
fps: f32,
|
||||
hints: &str,
|
||||
status: Option<&str>,
|
||||
) -> Line<'static> {
|
||||
let mut spans = Vec::new();
|
||||
if let Some(c) = city {
|
||||
@@ -169,7 +168,15 @@ pub fn status_bar(
|
||||
format!("· {fps:.0} fps "),
|
||||
Style::default().fg(DIM),
|
||||
));
|
||||
spans.push(Span::styled(format!("· {hints}"), Style::default().fg(DIM)));
|
||||
match status {
|
||||
Some(msg) => spans.push(Span::styled(
|
||||
format!("· {msg}"),
|
||||
Style::default()
|
||||
.fg(Color::Rgb(255, 200, 80))
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)),
|
||||
None => spans.push(Span::styled(format!("· {hints}"), Style::default().fg(DIM))),
|
||||
}
|
||||
Line::from(spans)
|
||||
}
|
||||
|
||||
@@ -198,8 +205,16 @@ mod tests {
|
||||
let sel = strata_selector(StratumKind::Skyways, 0);
|
||||
let text: String = sel.iter().map(|l| l.to_string() + "\n").collect();
|
||||
assert!(text.contains("Skyways") && text.contains("▌"));
|
||||
let bar = status_bar(Some(&c), StratumKind::Street, 30.0, "q quit");
|
||||
assert!(bar.to_string().contains(&c.name));
|
||||
let bar = status_bar(Some(&c), StratumKind::Street, 30.0, "q quit", None);
|
||||
assert!(bar.to_string().contains(&c.name) && bar.to_string().contains("q quit"));
|
||||
let bar = status_bar(
|
||||
Some(&c),
|
||||
StratumKind::Street,
|
||||
30.0,
|
||||
"q quit",
|
||||
Some("saved x"),
|
||||
);
|
||||
assert!(bar.to_string().contains("saved x") && !bar.to_string().contains("q quit"));
|
||||
assert!(!legend(StratumKind::Undercity).is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@ impl Player {
|
||||
let is_core = |p: Pos| {
|
||||
city.tile(street, p)
|
||||
.and_then(|t| t.district)
|
||||
.is_some_and(|d| city.district(d).theme == Theme::CorpCore)
|
||||
.and_then(|d| city.district_opt(d))
|
||||
.is_some_and(|d| d.theme == Theme::CorpCore)
|
||||
};
|
||||
let public: Vec<Pos> = city
|
||||
.connectors
|
||||
|
||||
Reference in New Issue
Block a user