src/world.rs 9.5 K · 332 lines · raw · history

1 use bevy::prelude::*;
2
3 pub const TILE_SIZE: f32 = 16.0;
4 pub const MAX_HEIGHT: f32 = 100.0;
5 pub const UPHILL_COST_PER_METER: f32 = 20.0;
6
7 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
8 pub enum Terrain {
9 Grass,
10 Soil,
11 Sand,
12 ShallowWater,
13 DeepWater,
14 Stone,
15 IronOre,
16 GoldOre,
17 Gravel,
18 }
19
20 impl Terrain {
21 pub const ALL: [Terrain; 9] = [
22 Terrain::Grass,
23 Terrain::Soil,
24 Terrain::Sand,
25 Terrain::ShallowWater,
26 Terrain::DeepWater,
27 Terrain::Stone,
28 Terrain::IronOre,
29 Terrain::GoldOre,
30 Terrain::Gravel,
31 ];
32
33 pub fn index(self) -> u16 {
34 self as u16
35 }
36
37 pub fn move_cost(self) -> Option<u32> {
38 match self {
39 Terrain::Grass | Terrain::Soil => Some(100),
40 Terrain::Sand | Terrain::Gravel => Some(130),
41 Terrain::ShallowWater => Some(300),
42 Terrain::DeepWater | Terrain::Stone | Terrain::IronOre | Terrain::GoldOre => None,
43 }
44 }
45
46 pub fn is_water(self) -> bool {
47 matches!(self, Terrain::ShallowWater | Terrain::DeepWater)
48 }
49
50 pub fn is_rock(self) -> bool {
51 matches!(self, Terrain::Stone | Terrain::IronOre | Terrain::GoldOre)
52 }
53
54 pub fn color(self) -> Srgba {
55 match self {
56 Terrain::Grass => Srgba::rgb_u8(92, 140, 64),
57 Terrain::Soil => Srgba::rgb_u8(122, 92, 60),
58 Terrain::Sand => Srgba::rgb_u8(214, 196, 140),
59 Terrain::ShallowWater => Srgba::rgb_u8(72, 132, 184),
60 Terrain::DeepWater => Srgba::rgb_u8(36, 76, 140),
61 Terrain::Stone | Terrain::IronOre | Terrain::GoldOre => Srgba::rgb_u8(118, 118, 124),
62 Terrain::Gravel => Srgba::rgb_u8(150, 146, 138),
63 }
64 }
65
66 pub fn speck_color(self) -> Option<Srgba> {
67 match self {
68 Terrain::IronOre => Some(Srgba::rgb_u8(178, 104, 72)),
69 Terrain::GoldOre => Some(Srgba::rgb_u8(236, 196, 60)),
70 _ => None,
71 }
72 }
73 }
74
75 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
76 pub enum Plant {
77 Tree,
78 BerryBush,
79 Crop,
80 }
81
82 impl Plant {
83 pub fn extra_move_cost(self) -> u32 {
84 match self {
85 Plant::Tree => 100,
86 Plant::BerryBush => 60,
87 Plant::Crop => 30,
88 }
89 }
90
91 pub fn ripening_ticks(self) -> Option<f32> {
92 let days = match self {
93 Plant::Tree => return None,
94 Plant::BerryBush => 2.0,
95 Plant::Crop => 1.5,
96 };
97 Some(days * 24.0 * crate::sim::TICKS_PER_HOUR as f32)
98 }
99
100 pub fn is_ripe(self, growth: f32) -> bool {
101 self.ripening_ticks().is_some() && growth >= 1.0
102 }
103 }
104
105 #[derive(Component, Default, Debug, Clone, Copy, PartialEq)]
106 pub struct TilePos(pub UVec2);
107
108 #[derive(Message, Debug, Clone, Copy, PartialEq)]
109 pub struct TileChanged(pub UVec2);
110
111 #[derive(Resource, Clone, Debug)]
112 pub struct WorldMap {
113 pub size: UVec2,
114 tiles: Vec<Terrain>,
115 plants: Vec<Option<Plant>>,
116 heights: Vec<f32>,
117 walls: Vec<bool>,
118 roofs: Vec<bool>,
119 growth: Vec<f32>,
120 }
121
122 impl WorldMap {
123 pub fn filled(size: UVec2, terrain: Terrain) -> Self {
124 let n = size.element_product() as usize;
125 Self {
126 size,
127 tiles: vec![terrain; n],
128 plants: vec![None; n],
129 heights: vec![0.0; n],
130 walls: vec![false; n],
131 roofs: vec![false; n],
132 growth: vec![0.0; n],
133 }
134 }
135
136 pub fn in_bounds(&self, pos: IVec2) -> bool {
137 pos.x >= 0 && pos.y >= 0 && (pos.x as u32) < self.size.x && (pos.y as u32) < self.size.y
138 }
139
140 fn index(&self, pos: UVec2) -> usize {
141 (pos.y * self.size.x + pos.x) as usize
142 }
143
144 pub fn get(&self, pos: IVec2) -> Option<Terrain> {
145 self.in_bounds(pos)
146 .then(|| self.tiles[self.index(pos.as_uvec2())])
147 }
148
149 pub fn set(&mut self, pos: UVec2, terrain: Terrain) {
150 let i = self.index(pos);
151 self.tiles[i] = terrain;
152 }
153
154 pub fn plant(&self, pos: IVec2) -> Option<Plant> {
155 self.in_bounds(pos)
156 .then(|| self.plants[self.index(pos.as_uvec2())])
157 .flatten()
158 }
159
160 pub fn set_plant(&mut self, pos: UVec2, plant: Option<Plant>) {
161 let i = self.index(pos);
162 self.plants[i] = plant;
163 }
164
165 pub fn height(&self, pos: IVec2) -> Option<f32> {
166 self.in_bounds(pos)
167 .then(|| self.heights[self.index(pos.as_uvec2())])
168 }
169
170 pub fn set_height(&mut self, pos: UVec2, height: f32) {
171 let i = self.index(pos);
172 self.heights[i] = height;
173 }
174
175 pub fn is_wall(&self, pos: IVec2) -> bool {
176 self.in_bounds(pos) && self.walls[self.index(pos.as_uvec2())]
177 }
178
179 pub fn set_wall(&mut self, pos: UVec2, wall: bool) {
180 let i = self.index(pos);
181 self.walls[i] = wall;
182 }
183
184 pub fn growth(&self, pos: IVec2) -> f32 {
185 if self.in_bounds(pos) {
186 self.growth[self.index(pos.as_uvec2())]
187 } else {
188 0.0
189 }
190 }
191
192 pub fn set_growth(&mut self, pos: UVec2, growth: f32) {
193 let i = self.index(pos);
194 self.growth[i] = growth.clamp(0.0, 1.0);
195 }
196
197 pub fn ripe_plant(&self, pos: IVec2) -> Option<Plant> {
198 self.plant(pos).filter(|p| p.is_ripe(self.growth(pos)))
199 }
200
201 pub fn is_roofed(&self, pos: IVec2) -> bool {
202 self.in_bounds(pos) && self.roofs[self.index(pos.as_uvec2())]
203 }
204
205 pub fn set_roof(&mut self, pos: UVec2, roof: bool) {
206 let i = self.index(pos);
207 self.roofs[i] = roof;
208 }
209
210 pub fn move_cost(&self, pos: IVec2) -> Option<u32> {
211 if self.is_wall(pos) {
212 return None;
213 }
214 let terrain = self.get(pos)?.move_cost()?;
215 Some(terrain + self.plant(pos).map_or(0, Plant::extra_move_cost))
216 }
217
218 pub fn step_cost(&self, from: IVec2, to: IVec2) -> Option<u32> {
219 let climb = (self.height(to)? - self.height(from)?).max(0.0);
220 Some(self.move_cost(to)? + (climb * UPHILL_COST_PER_METER).round() as u32)
221 }
222
223 pub fn positions(&self) -> impl Iterator<Item = UVec2> + use<> {
224 let size = self.size;
225 (0..size.y).flat_map(move |y| (0..size.x).map(move |x| UVec2::new(x, y)))
226 }
227
228 pub fn world_size(&self) -> Vec2 {
229 self.size.as_vec2() * TILE_SIZE
230 }
231
232 pub fn tile_center(pos: UVec2) -> Vec2 {
233 (pos.as_vec2() + 0.5) * TILE_SIZE
234 }
235
236 pub fn tile_at(&self, world: Vec2) -> Option<UVec2> {
237 let pos = (world / TILE_SIZE).floor().as_ivec2();
238 self.in_bounds(pos).then(|| pos.as_uvec2())
239 }
240 }
241
242 pub fn hash2(x: u32, y: u32) -> u32 {
243 let mut h = x.wrapping_mul(0x9E37_79B1) ^ y.wrapping_mul(0x85EB_CA77);
244 h ^= h >> 15;
245 h = h.wrapping_mul(0x2C1B_3C6D);
246 h ^= h >> 12;
247 h = h.wrapping_mul(0x297A_2D39);
248 h ^ (h >> 15)
249 }
250
251 #[cfg(test)]
252 mod tests {
253 use super::*;
254
255 #[test]
256 fn get_out_of_bounds_is_none() {
257 let map = WorldMap::filled(UVec2::new(4, 3), Terrain::Grass);
258 assert_eq!(map.get(IVec2::new(-1, 0)), None);
259 assert_eq!(map.get(IVec2::new(4, 0)), None);
260 assert_eq!(map.get(IVec2::new(0, 3)), None);
261 assert_eq!(map.get(IVec2::new(3, 2)), Some(Terrain::Grass));
262 }
263
264 #[test]
265 fn tiles_are_stored_row_major_from_bottom_left() {
266 let mut map = WorldMap::filled(UVec2::new(4, 3), Terrain::Grass);
267 map.set(UVec2::new(1, 2), Terrain::Stone);
268 assert_eq!(map.tiles[2 * 4 + 1], Terrain::Stone);
269 }
270
271 #[test]
272 fn tile_at_inverts_tile_center() {
273 let map = WorldMap::filled(UVec2::new(32, 128), Terrain::Grass);
274 for pos in [UVec2::ZERO, UVec2::new(17, 99), map.size - 1] {
275 assert_eq!(map.tile_at(WorldMap::tile_center(pos)), Some(pos));
276 }
277 }
278
279 #[test]
280 fn tile_at_outside_world_is_none() {
281 let map = WorldMap::filled(UVec2::new(32, 128), Terrain::Grass);
282 assert_eq!(map.tile_at(Vec2::new(-0.1, 0.0)), None);
283 assert_eq!(map.tile_at(map.world_size()), None);
284 }
285
286 #[test]
287 fn trees_slow_movement_but_do_not_block_it() {
288 let mut map = WorldMap::filled(UVec2::new(2, 1), Terrain::Grass);
289 let open = map.move_cost(IVec2::ZERO).unwrap();
290 map.set_plant(UVec2::ZERO, Some(Plant::Tree));
291 assert!(map.move_cost(IVec2::ZERO).unwrap() > open);
292 }
293
294 #[test]
295 fn bushes_and_crops_ripen_but_trees_do_not() {
296 assert!(Plant::BerryBush.is_ripe(1.0));
297 assert!(!Plant::Crop.is_ripe(0.9));
298 assert!(!Plant::Tree.is_ripe(1.0));
299 }
300
301 #[test]
302 fn walls_block_movement() {
303 let mut map = WorldMap::filled(UVec2::new(2, 1), Terrain::Grass);
304 map.set_wall(UVec2::ZERO, true);
305 assert_eq!(map.move_cost(IVec2::ZERO), None);
306 assert!(map.move_cost(IVec2::X).is_some());
307 }
308
309 #[test]
310 fn walking_uphill_costs_more_than_downhill_or_flat() {
311 let mut map = WorldMap::filled(UVec2::new(3, 1), Terrain::Grass);
312 map.set_height(UVec2::new(1, 0), 5.0);
313 let (low, high, flat) = (IVec2::new(0, 0), IVec2::new(1, 0), IVec2::new(2, 0));
314 let up = map.step_cost(low, high).unwrap();
315 let down = map.step_cost(high, low).unwrap();
316 assert!(up > down);
317 assert_eq!(down, map.step_cost(low, flat).unwrap());
318 }
319
320 #[test]
321 fn rock_and_water_classification() {
322 assert!(Terrain::IronOre.is_rock() && Terrain::IronOre.move_cost().is_none());
323 assert!(Terrain::ShallowWater.is_water() && !Terrain::ShallowWater.is_rock());
324 }
325
326 #[test]
327 fn terrain_index_matches_position_in_all() {
328 for (i, t) in Terrain::ALL.iter().enumerate() {
329 assert_eq!(t.index() as usize, i);
330 }
331 }
332 }