use flatland_protocol::{
humanize_snake_id, BiomeZoneView, GrowthZoneView, PropertyZoneView, TaxZoneView,
ZoneRectView,
};
use crate::GameState;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum WorldZoneLayer {
Growth,
Biome,
Claim,
Tax,
YourPlot,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorldZoneChip {
pub layer: WorldZoneLayer,
pub text: String,
}
pub fn zone_friendly_label(id: &str, label: Option<&str>) -> String {
label
.map(str::trim)
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.unwrap_or_else(|| humanize_snake_id(&id.replace('-', "_")))
}
pub(crate) fn zone_rects_contain(rects: &[ZoneRectView], x: f32, y: f32) -> bool {
rects
.iter()
.any(|r| x >= r.x0 && x <= r.x1 && y >= r.y0 && y <= r.y1)
}
fn zone_at<'a, Z>(
zones: &'a [Z],
x: f32,
y: f32,
rects: impl Fn(&Z) -> &[ZoneRectView],
z_order: impl Fn(&Z) -> i32,
) -> Option<&'a Z> {
zones
.iter()
.enumerate()
.filter(|(_, z)| zone_rects_contain(rects(z), x, y))
.max_by(|(ia, a), (ib, b)| z_order(a).cmp(&z_order(b)).then(ia.cmp(ib)))
.map(|(_, z)| z)
}
impl GameState {
pub fn growth_zone_at(&self, x: f32, y: f32) -> Option<&GrowthZoneView> {
zone_at(
&self.growth_zones,
x,
y,
|z| &z.rects,
|z| z.z_order,
)
}
pub fn biome_zone_at(&self, x: f32, y: f32) -> Option<&BiomeZoneView> {
zone_at(
&self.biome_zones,
x,
y,
|z| &z.rects,
|z| z.z_order,
)
}
pub fn world_zone_chips_at_player(&self) -> Vec<WorldZoneChip> {
let (x, y) = self.player_position();
self.world_zone_chips_at(x, y)
}
pub fn world_zone_chips_at(&self, x: f32, y: f32) -> Vec<WorldZoneChip> {
let mut chips = Vec::new();
if let Some(z) = self.growth_zone_at(x, y) {
let name = zone_friendly_label(&z.id, z.label.as_deref());
let detail = if (z.fertility - 1.0).abs() > 0.05 {
format!("{name} ({:.1}× growth)", z.fertility)
} else {
name
};
chips.push(WorldZoneChip {
layer: WorldZoneLayer::Growth,
text: format!("Growth: {detail}"),
});
}
if let Some(z) = self.biome_zone_at(x, y) {
let name = z
.label
.as_deref()
.filter(|s| !s.trim().is_empty())
.map(|s| s.to_string())
.unwrap_or_else(|| humanize_snake_id(&z.biome_id));
chips.push(WorldZoneChip {
layer: WorldZoneLayer::Biome,
text: format!("Biome: {name}"),
});
}
if let Some(z) = self.property_zone_at(x, y) {
chips.push(WorldZoneChip {
layer: WorldZoneLayer::Claim,
text: format!("Claim: {}", property_zone_chip_label(z)),
});
}
if let Some(z) = self.tax_zone_at(x, y) {
chips.push(WorldZoneChip {
layer: WorldZoneLayer::Tax,
text: format!("Tax: {}", tax_zone_chip_label(z)),
});
}
if let Some(plot) = self
.property_plots
.iter()
.find(|p| p.is_mine && point_in_plot(x, y, p))
{
let name = plot
.zone_label
.as_deref()
.filter(|s| !s.trim().is_empty())
.map(|s| s.to_string())
.unwrap_or_else(|| "your plot".to_string());
chips.push(WorldZoneChip {
layer: WorldZoneLayer::YourPlot,
text: format!("Plot: {name}"),
});
}
chips.sort_by_key(|c| c.layer);
chips
}
pub fn world_zones_status_line(&self) -> String {
self.world_zone_chips_at_player()
.into_iter()
.map(|c| c.text)
.collect::<Vec<_>>()
.join(" · ")
}
}
fn property_zone_chip_label(zone: &PropertyZoneView) -> String {
zone_friendly_label(&zone.id, zone.label.as_deref())
}
fn tax_zone_chip_label(zone: &TaxZoneView) -> String {
let name = zone_friendly_label(&zone.id, zone.label.as_deref());
let mut parts = vec![name];
if zone.rate_bps > 0 {
let pct = zone.rate_bps as f32 / 100.0;
if (pct - pct.round()).abs() < 0.05 {
parts.push(format!("{:.0}%", pct));
} else {
parts.push(format!("{pct:.1}%"));
}
}
if zone.flat_copper > 0 {
parts.push(format!("+{}¢", zone.flat_copper));
}
parts.join(" ")
}
fn point_in_plot(x: f32, y: f32, plot: &flatland_protocol::PropertyPlotView) -> bool {
x >= plot.x0 && x < plot.x1 && y >= plot.y0 && y < plot.y1
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tax_chip_formats_rate() {
let zone = TaxZoneView {
id: "mill-fields-tax".into(),
label: Some("Mill fields tax".into()),
rects: vec![],
z_order: 0,
rate_bps: 400,
flat_copper: 0,
market_sales_tax_bps: 0,
market_sales_flat_copper: 0,
};
assert_eq!(tax_zone_chip_label(&zone), "Mill fields tax 4%");
}
#[test]
fn zone_friendly_label_prefers_designer_label() {
assert_eq!(
zone_friendly_label("mill-fields-growth", Some("Mill fields")),
"Mill fields"
);
assert_eq!(
zone_friendly_label("mill-fields-growth", None),
"Mill Fields Growth"
);
}
}