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