1use canwu_core::{ArmyId, GovernmentId, LetterId, 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 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub transit: Option<PersonTransitState>,
22}
23
24#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
25pub struct PersonTransitState {
26 pub from: TerritoryId,
27 pub to: TerritoryId,
28 pub departed_at: SimTime,
29 pub arrives_at: SimTime,
30}
31
32#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
33#[serde(rename_all = "snake_case")]
34pub enum LetterStatus {
35 HeldByPerson,
36 InTransit,
37 HeldAtLocation,
38 Delivered,
39}
40
41#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
42pub struct LetterCargo {
43 pub id: LetterId,
44 pub sender: PersonId,
45 pub recipient: PersonId,
46 pub body: String,
47 pub status: LetterStatus,
48 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub carrier: Option<PersonId>,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub location: Option<TerritoryId>,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub delivered_at: Option<SimTime>,
54}
55
56#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
57pub struct Government {
58 pub id: GovernmentId,
59 pub name: String,
60 pub capital: TerritoryId,
61}
62
63#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
64pub struct Territory {
65 pub id: TerritoryId,
66 pub name: String,
67 pub controller: GovernmentId,
68 pub position: MapPoint,
69}
70
71#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
72pub struct Route {
73 pub id: RouteId,
74 pub name: String,
75 pub from: TerritoryId,
76 pub to: TerritoryId,
77 pub travel_minutes: i64,
78 pub terrain: String,
79}
80
81impl Route {
82 #[must_use]
83 pub fn connects(&self, first: TerritoryId, second: TerritoryId) -> bool {
84 (self.from == first && self.to == second) || (self.from == second && self.to == first)
85 }
86
87 #[must_use]
88 pub fn other_end(&self, territory: TerritoryId) -> Option<TerritoryId> {
89 if self.from == territory {
90 Some(self.to)
91 } else if self.to == territory {
92 Some(self.from)
93 } else {
94 None
95 }
96 }
97}
98
99#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
100pub struct TransitState {
101 pub from: TerritoryId,
102 pub to: TerritoryId,
103 pub departed_at: SimTime,
104 pub arrives_at: SimTime,
105}
106
107#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
108pub struct Army {
109 pub id: ArmyId,
110 pub name: String,
111 pub government: GovernmentId,
112 pub commander: PersonId,
113 pub location: TerritoryId,
114 pub strength: u32,
115 pub morale: u16,
116 pub transit: Option<TransitState>,
117}
118
119#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
120pub struct WorldSnapshot {
121 pub people: Vec<Person>,
122 pub governments: Vec<Government>,
123 pub territories: Vec<Territory>,
124 pub routes: Vec<Route>,
125 pub armies: Vec<Army>,
126 #[serde(default, skip_serializing_if = "Vec::is_empty")]
127 pub letters: Vec<LetterCargo>,
128}
129
130impl WorldSnapshot {
131 #[must_use]
132 pub fn person(&self, id: PersonId) -> Option<&Person> {
133 self.people.iter().find(|person| person.id == id)
134 }
135
136 #[must_use]
137 pub fn government(&self, id: GovernmentId) -> Option<&Government> {
138 self.governments
139 .iter()
140 .find(|government| government.id == id)
141 }
142
143 #[must_use]
144 pub fn territory(&self, id: TerritoryId) -> Option<&Territory> {
145 self.territories.iter().find(|territory| territory.id == id)
146 }
147
148 #[must_use]
149 pub fn route(&self, id: RouteId) -> Option<&Route> {
150 self.routes.iter().find(|route| route.id == id)
151 }
152
153 #[must_use]
154 pub fn army(&self, id: ArmyId) -> Option<&Army> {
155 self.armies.iter().find(|army| army.id == id)
156 }
157
158 #[must_use]
159 pub fn letter(&self, id: LetterId) -> Option<&LetterCargo> {
160 self.letters.iter().find(|letter| letter.id == id)
161 }
162
163 #[must_use]
164 pub fn route_between(&self, from: TerritoryId, to: TerritoryId) -> Option<&Route> {
165 self.routes.iter().find(|route| route.connects(from, to))
166 }
167
168 pub fn adjacent_territories(
169 &self,
170 territory: TerritoryId,
171 ) -> impl Iterator<Item = TerritoryId> + '_ {
172 self.routes
173 .iter()
174 .filter_map(move |route| route.other_end(territory))
175 }
176
177 #[must_use]
178 pub fn travel_duration(&self, from: TerritoryId, to: TerritoryId) -> Option<SimDuration> {
179 self.route_between(from, to)
180 .map(|route| SimDuration::minutes(route.travel_minutes))
181 }
182
183 #[must_use]
184 pub fn distance(&self, from: TerritoryId, to: TerritoryId) -> Option<f32> {
185 let first = self.territory(from)?.position;
186 let second = self.territory(to)?.position;
187 Some((second.x - first.x).hypot(second.y - first.y))
188 }
189
190 pub fn territories_within(
191 &self,
192 center: TerritoryId,
193 radius: f32,
194 ) -> impl Iterator<Item = &Territory> {
195 let center_position = self.territory(center).map(|territory| territory.position);
196 self.territories.iter().filter(move |territory| {
197 center_position.is_some_and(|point| {
198 (territory.position.x - point.x).hypot(territory.position.y - point.y) <= radius
199 })
200 })
201 }
202}
203
204#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
205pub struct WorldDiff {
206 pub changed_armies: Vec<ArmyId>,
207 pub changed_people: Vec<PersonId>,
208 pub changed_territories: Vec<TerritoryId>,
209 pub changed_letters: Vec<LetterId>,
210}
211
212impl WorldDiff {
213 #[must_use]
214 pub fn between(before: &WorldSnapshot, after: &WorldSnapshot) -> Self {
215 let changed_armies = after
216 .armies
217 .iter()
218 .filter(|army| before.army(army.id) != Some(*army))
219 .map(|army| army.id)
220 .collect();
221 let changed_people = after
222 .people
223 .iter()
224 .filter(|person| before.person(person.id) != Some(*person))
225 .map(|person| person.id)
226 .collect();
227 let changed_territories = after
228 .territories
229 .iter()
230 .filter(|territory| before.territory(territory.id) != Some(*territory))
231 .map(|territory| territory.id)
232 .collect();
233 let changed_letters = after
234 .letters
235 .iter()
236 .filter(|letter| before.letter(letter.id) != Some(*letter))
237 .map(|letter| letter.id)
238 .collect();
239 Self {
240 changed_armies,
241 changed_people,
242 changed_territories,
243 changed_letters,
244 }
245 }
246}