Skip to main content

canwu_world/
lib.rs

1//! Historical entity read models and detached world snapshots.
2
3use canwu_core::{ArmyId, GovernmentId, PersonId, RouteId, TerritoryId};
4use canwu_time::{SimDuration, SimTime};
5use serde::{Deserialize, Serialize};
6
7#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)]
8pub struct MapPoint {
9    pub x: f32,
10    pub y: f32,
11}
12
13#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
14pub struct Person {
15    pub id: PersonId,
16    pub name: String,
17    pub government: GovernmentId,
18    pub current_location: TerritoryId,
19    pub roles: Vec<String>,
20}
21
22#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
23pub struct Government {
24    pub id: GovernmentId,
25    pub name: String,
26    pub capital: TerritoryId,
27}
28
29#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
30pub struct Territory {
31    pub id: TerritoryId,
32    pub name: String,
33    pub controller: GovernmentId,
34    pub position: MapPoint,
35}
36
37#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
38pub struct Route {
39    pub id: RouteId,
40    pub name: String,
41    pub from: TerritoryId,
42    pub to: TerritoryId,
43    pub travel_minutes: i64,
44    pub terrain: String,
45}
46
47impl Route {
48    #[must_use]
49    pub fn connects(&self, first: TerritoryId, second: TerritoryId) -> bool {
50        (self.from == first && self.to == second) || (self.from == second && self.to == first)
51    }
52
53    #[must_use]
54    pub fn other_end(&self, territory: TerritoryId) -> Option<TerritoryId> {
55        if self.from == territory {
56            Some(self.to)
57        } else if self.to == territory {
58            Some(self.from)
59        } else {
60            None
61        }
62    }
63}
64
65#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
66pub struct TransitState {
67    pub from: TerritoryId,
68    pub to: TerritoryId,
69    pub departed_at: SimTime,
70    pub arrives_at: SimTime,
71}
72
73#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
74pub struct Army {
75    pub id: ArmyId,
76    pub name: String,
77    pub government: GovernmentId,
78    pub commander: PersonId,
79    pub location: TerritoryId,
80    pub strength: u32,
81    pub morale: u16,
82    pub transit: Option<TransitState>,
83}
84
85#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
86pub struct WorldSnapshot {
87    pub people: Vec<Person>,
88    pub governments: Vec<Government>,
89    pub territories: Vec<Territory>,
90    pub routes: Vec<Route>,
91    pub armies: Vec<Army>,
92}
93
94impl WorldSnapshot {
95    #[must_use]
96    pub fn person(&self, id: PersonId) -> Option<&Person> {
97        self.people.iter().find(|person| person.id == id)
98    }
99
100    #[must_use]
101    pub fn government(&self, id: GovernmentId) -> Option<&Government> {
102        self.governments
103            .iter()
104            .find(|government| government.id == id)
105    }
106
107    #[must_use]
108    pub fn territory(&self, id: TerritoryId) -> Option<&Territory> {
109        self.territories.iter().find(|territory| territory.id == id)
110    }
111
112    #[must_use]
113    pub fn route(&self, id: RouteId) -> Option<&Route> {
114        self.routes.iter().find(|route| route.id == id)
115    }
116
117    #[must_use]
118    pub fn army(&self, id: ArmyId) -> Option<&Army> {
119        self.armies.iter().find(|army| army.id == id)
120    }
121
122    #[must_use]
123    pub fn route_between(&self, from: TerritoryId, to: TerritoryId) -> Option<&Route> {
124        self.routes.iter().find(|route| route.connects(from, to))
125    }
126
127    pub fn adjacent_territories(
128        &self,
129        territory: TerritoryId,
130    ) -> impl Iterator<Item = TerritoryId> + '_ {
131        self.routes
132            .iter()
133            .filter_map(move |route| route.other_end(territory))
134    }
135
136    #[must_use]
137    pub fn travel_duration(&self, from: TerritoryId, to: TerritoryId) -> Option<SimDuration> {
138        self.route_between(from, to)
139            .map(|route| SimDuration::minutes(route.travel_minutes))
140    }
141
142    #[must_use]
143    pub fn distance(&self, from: TerritoryId, to: TerritoryId) -> Option<f32> {
144        let first = self.territory(from)?.position;
145        let second = self.territory(to)?.position;
146        Some((second.x - first.x).hypot(second.y - first.y))
147    }
148
149    pub fn territories_within(
150        &self,
151        center: TerritoryId,
152        radius: f32,
153    ) -> impl Iterator<Item = &Territory> {
154        let center_position = self.territory(center).map(|territory| territory.position);
155        self.territories.iter().filter(move |territory| {
156            center_position.is_some_and(|point| {
157                (territory.position.x - point.x).hypot(territory.position.y - point.y) <= radius
158            })
159        })
160    }
161}
162
163#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
164pub struct WorldDiff {
165    pub changed_armies: Vec<ArmyId>,
166    pub changed_people: Vec<PersonId>,
167    pub changed_territories: Vec<TerritoryId>,
168}
169
170impl WorldDiff {
171    #[must_use]
172    pub fn between(before: &WorldSnapshot, after: &WorldSnapshot) -> Self {
173        let changed_armies = after
174            .armies
175            .iter()
176            .filter(|army| before.army(army.id) != Some(*army))
177            .map(|army| army.id)
178            .collect();
179        let changed_people = after
180            .people
181            .iter()
182            .filter(|person| before.person(person.id) != Some(*person))
183            .map(|person| person.id)
184            .collect();
185        let changed_territories = after
186            .territories
187            .iter()
188            .filter(|territory| before.territory(territory.id) != Some(*territory))
189            .map(|territory| territory.id)
190            .collect();
191        Self {
192            changed_armies,
193            changed_people,
194            changed_territories,
195        }
196    }
197}