1use 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}