src/world.rs 7.5 K · 262 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 }
18
19 impl Terrain {
20 pub const ALL: [Terrain; 8] = [
21 Terrain::Grass,
22 Terrain::Soil,
23 Terrain::Sand,
24 Terrain::ShallowWater,
25 Terrain::DeepWater,
26 Terrain::Stone,
27 Terrain::IronOre,
28 Terrain::GoldOre,
29 ];
30
31 pub fn index(self) -> u16 {
32 self as u16
33 }
34
35 pub fn move_cost(self) -> Option<u32> {
36 match self {
37 Terrain::Grass | Terrain::Soil => Some(100),
38 Terrain::Sand => Some(130),
39 Terrain::ShallowWater => Some(300),
40 Terrain::DeepWater | Terrain::Stone | Terrain::IronOre | Terrain::GoldOre => None,
41 }
42 }
43
44 pub fn is_water(self) -> bool {
45 matches!(self, Terrain::ShallowWater | Terrain::DeepWater)
46 }
47
48 pub fn is_rock(self) -> bool {
49 matches!(self, Terrain::Stone | Terrain::IronOre | Terrain::GoldOre)
50 }
51
52 pub fn color(self) -> Srgba {
53 match self {
54 Terrain::Grass => Srgba::rgb_u8(92, 140, 64),
55 Terrain::Soil => Srgba::rgb_u8(122, 92, 60),
56 Terrain::Sand => Srgba::rgb_u8(214, 196, 140),
57 Terrain::ShallowWater => Srgba::rgb_u8(72, 132, 184),
58 Terrain::DeepWater => Srgba::rgb_u8(36, 76, 140),
59 Terrain::Stone | Terrain::IronOre | Terrain::GoldOre => Srgba::rgb_u8(118, 118, 124),
60 }
61 }
62
63 pub fn speck_color(self) -> Option<Srgba> {
64 match self {
65 Terrain::IronOre => Some(Srgba::rgb_u8(178, 104, 72)),
66 Terrain::GoldOre => Some(Srgba::rgb_u8(236, 196, 60)),
67 _ => None,
68 }
69 }
70 }
71
72 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
73 pub enum Plant {
74 Tree,
75 }
76
77 impl Plant {
78 pub const ALL: [Plant; 1] = [Plant::Tree];
79
80 pub fn index(self) -> u16 {
81 self as u16
82 }
83
84 pub fn extra_move_cost(self) -> u32 {
85 match self {
86 Plant::Tree => 100,
87 }
88 }
89 }
90
91 #[derive(Component, Default, Debug, Clone, Copy, PartialEq)]
92 pub struct TilePos(pub UVec2);
93
94 #[derive(Message, Debug, Clone, Copy, PartialEq)]
95 pub struct TileChanged(pub UVec2);
96
97 #[derive(Resource, Clone, Debug)]
98 pub struct WorldMap {
99 pub size: UVec2,
100 tiles: Vec<Terrain>,
101 plants: Vec<Option<Plant>>,
102 heights: Vec<f32>,
103 }
104
105 impl WorldMap {
106 pub fn filled(size: UVec2, terrain: Terrain) -> Self {
107 let n = size.element_product() as usize;
108 Self {
109 size,
110 tiles: vec![terrain; n],
111 plants: vec![None; n],
112 heights: vec![0.0; n],
113 }
114 }
115
116 pub fn in_bounds(&self, pos: IVec2) -> bool {
117 pos.x >= 0 && pos.y >= 0 && (pos.x as u32) < self.size.x && (pos.y as u32) < self.size.y
118 }
119
120 fn index(&self, pos: UVec2) -> usize {
121 (pos.y * self.size.x + pos.x) as usize
122 }
123
124 pub fn get(&self, pos: IVec2) -> Option<Terrain> {
125 self.in_bounds(pos)
126 .then(|| self.tiles[self.index(pos.as_uvec2())])
127 }
128
129 pub fn set(&mut self, pos: UVec2, terrain: Terrain) {
130 let i = self.index(pos);
131 self.tiles[i] = terrain;
132 }
133
134 pub fn plant(&self, pos: IVec2) -> Option<Plant> {
135 self.in_bounds(pos)
136 .then(|| self.plants[self.index(pos.as_uvec2())])
137 .flatten()
138 }
139
140 pub fn set_plant(&mut self, pos: UVec2, plant: Option<Plant>) {
141 let i = self.index(pos);
142 self.plants[i] = plant;
143 }
144
145 pub fn height(&self, pos: IVec2) -> Option<f32> {
146 self.in_bounds(pos)
147 .then(|| self.heights[self.index(pos.as_uvec2())])
148 }
149
150 pub fn set_height(&mut self, pos: UVec2, height: f32) {
151 let i = self.index(pos);
152 self.heights[i] = height;
153 }
154
155 pub fn move_cost(&self, pos: IVec2) -> Option<u32> {
156 let terrain = self.get(pos)?.move_cost()?;
157 Some(terrain + self.plant(pos).map_or(0, Plant::extra_move_cost))
158 }
159
160 pub fn step_cost(&self, from: IVec2, to: IVec2) -> Option<u32> {
161 let climb = (self.height(to)? - self.height(from)?).max(0.0);
162 Some(self.move_cost(to)? + (climb * UPHILL_COST_PER_METER).round() as u32)
163 }
164
165 pub fn positions(&self) -> impl Iterator<Item = UVec2> + use<> {
166 let size = self.size;
167 (0..size.y).flat_map(move |y| (0..size.x).map(move |x| UVec2::new(x, y)))
168 }
169
170 pub fn world_size(&self) -> Vec2 {
171 self.size.as_vec2() * TILE_SIZE
172 }
173
174 pub fn tile_center(pos: UVec2) -> Vec2 {
175 (pos.as_vec2() + 0.5) * TILE_SIZE
176 }
177
178 pub fn tile_at(&self, world: Vec2) -> Option<UVec2> {
179 let pos = (world / TILE_SIZE).floor().as_ivec2();
180 self.in_bounds(pos).then(|| pos.as_uvec2())
181 }
182 }
183
184 pub fn hash2(x: u32, y: u32) -> u32 {
185 let mut h = x.wrapping_mul(0x9E37_79B1) ^ y.wrapping_mul(0x85EB_CA77);
186 h ^= h >> 15;
187 h = h.wrapping_mul(0x2C1B_3C6D);
188 h ^= h >> 12;
189 h = h.wrapping_mul(0x297A_2D39);
190 h ^ (h >> 15)
191 }
192
193 #[cfg(test)]
194 mod tests {
195 use super::*;
196
197 #[test]
198 fn get_out_of_bounds_is_none() {
199 let map = WorldMap::filled(UVec2::new(4, 3), Terrain::Grass);
200 assert_eq!(map.get(IVec2::new(-1, 0)), None);
201 assert_eq!(map.get(IVec2::new(4, 0)), None);
202 assert_eq!(map.get(IVec2::new(0, 3)), None);
203 assert_eq!(map.get(IVec2::new(3, 2)), Some(Terrain::Grass));
204 }
205
206 #[test]
207 fn tiles_are_stored_row_major_from_bottom_left() {
208 let mut map = WorldMap::filled(UVec2::new(4, 3), Terrain::Grass);
209 map.set(UVec2::new(1, 2), Terrain::Stone);
210 assert_eq!(map.tiles[2 * 4 + 1], Terrain::Stone);
211 }
212
213 #[test]
214 fn tile_at_inverts_tile_center() {
215 let map = WorldMap::filled(UVec2::new(32, 128), Terrain::Grass);
216 for pos in [UVec2::ZERO, UVec2::new(17, 99), map.size - 1] {
217 assert_eq!(map.tile_at(WorldMap::tile_center(pos)), Some(pos));
218 }
219 }
220
221 #[test]
222 fn tile_at_outside_world_is_none() {
223 let map = WorldMap::filled(UVec2::new(32, 128), Terrain::Grass);
224 assert_eq!(map.tile_at(Vec2::new(-0.1, 0.0)), None);
225 assert_eq!(map.tile_at(map.world_size()), None);
226 }
227
228 #[test]
229 fn trees_slow_movement_but_do_not_block_it() {
230 let mut map = WorldMap::filled(UVec2::new(2, 1), Terrain::Grass);
231 let open = map.move_cost(IVec2::ZERO).unwrap();
232 map.set_plant(UVec2::ZERO, Some(Plant::Tree));
233 assert!(map.move_cost(IVec2::ZERO).unwrap() > open);
234 }
235
236 #[test]
237 fn walking_uphill_costs_more_than_downhill_or_flat() {
238 let mut map = WorldMap::filled(UVec2::new(3, 1), Terrain::Grass);
239 map.set_height(UVec2::new(1, 0), 5.0);
240 let (low, high, flat) = (IVec2::new(0, 0), IVec2::new(1, 0), IVec2::new(2, 0));
241 let up = map.step_cost(low, high).unwrap();
242 let down = map.step_cost(high, low).unwrap();
243 assert!(up > down);
244 assert_eq!(down, map.step_cost(low, flat).unwrap());
245 }
246
247 #[test]
248 fn rock_and_water_classification() {
249 assert!(Terrain::IronOre.is_rock() && Terrain::IronOre.move_cost().is_none());
250 assert!(Terrain::ShallowWater.is_water() && !Terrain::ShallowWater.is_rock());
251 }
252
253 #[test]
254 fn terrain_index_matches_position_in_all() {
255 for (i, t) in Terrain::ALL.iter().enumerate() {
256 assert_eq!(t.index() as usize, i);
257 }
258 for (i, p) in Plant::ALL.iter().enumerate() {
259 assert_eq!(p.index() as usize, i);
260 }
261 }
262 }