src/structures.rs 10.3 K · 337 lines · raw · history

1 use bevy::prelude::*;
2 use serde::{Deserialize, Serialize};
3
4 use crate::{
5 items::ItemKind,
6 sim::{SimSystems, TICKS_PER_HOUR},
7 world::{TILE_SIZE, TilePos, WorldMap},
8 worldgen::WorldGenerated,
9 };
10
11 pub struct StructuresPlugin;
12
13 impl Plugin for StructuresPlugin {
14 fn build(&self, app: &mut App) {
15 app.add_systems(Startup, create_structure_assets)
16 .add_systems(
17 Update,
18 clear_structures.run_if(on_message::<WorldGenerated>),
19 )
20 .add_systems(FixedUpdate, burn_fuel.in_set(SimSystems::Act));
21 }
22 }
23
24 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
25 pub enum StructureKind {
26 Campfire,
27 Torch,
28 Bed,
29 Wall,
30 Door,
31 Workbench,
32 Statue,
33 }
34
35 impl StructureKind {
36 pub fn cost(self) -> (ItemKind, u32) {
37 match self {
38 StructureKind::Campfire => (ItemKind::Wood, 10),
39 StructureKind::Torch => (ItemKind::Wood, 2),
40 StructureKind::Bed => (ItemKind::Wood, 15),
41 StructureKind::Wall => (ItemKind::Stone, 2),
42 StructureKind::Door => (ItemKind::Wood, 5),
43 StructureKind::Workbench => (ItemKind::Wood, 20),
44 StructureKind::Statue => (ItemKind::Gold, 5),
45 }
46 }
47
48 pub fn fuel_ticks(self) -> Option<u32> {
49 let hours = match self {
50 StructureKind::Campfire => 12,
51 StructureKind::Torch => 8,
52 _ => return None,
53 };
54 Some(hours * TICKS_PER_HOUR as u32)
55 }
56
57 pub fn light(self) -> Option<(f32, Vec3)> {
58 match self {
59 StructureKind::Campfire => Some((7.0, Vec3::new(1.1, 0.8, 0.5))),
60 StructureKind::Torch => Some((4.0, Vec3::new(1.0, 0.8, 0.55))),
61 _ => None,
62 }
63 }
64
65 pub fn heat(self) -> f32 {
66 match self {
67 StructureKind::Campfire => 20.0,
68 StructureKind::Torch => 4.0,
69 _ => 0.0,
70 }
71 }
72
73 pub fn radius(self) -> f32 {
74 self.light().map_or(0.0, |(radius, _)| radius)
75 }
76
77 pub fn name(self) -> &'static str {
78 match self {
79 StructureKind::Campfire => "campfire",
80 StructureKind::Torch => "torch",
81 StructureKind::Bed => "bed",
82 StructureKind::Wall => "wall",
83 StructureKind::Door => "door",
84 StructureKind::Workbench => "workbench",
85 StructureKind::Statue => "statue",
86 }
87 }
88 }
89
90 #[derive(Component, Debug, Clone, Copy, PartialEq)]
91 #[require(Transform, Visibility)]
92 pub struct Structure {
93 pub kind: StructureKind,
94 }
95
96 #[derive(Component, Debug, Clone, Copy, PartialEq)]
97 pub struct Fuel(pub u32);
98
99 fn burn_fuel(mut commands: Commands, mut fuel: Query<(Entity, &mut Fuel)>) {
100 for (entity, mut fuel) in &mut fuel {
101 fuel.0 = fuel.0.saturating_sub(1);
102 if fuel.0 == 0 {
103 commands.entity(entity).despawn();
104 }
105 }
106 }
107
108 fn clear_structures(mut commands: Commands, structures: Query<Entity, With<Structure>>) {
109 for entity in &structures {
110 commands.entity(entity).despawn();
111 }
112 }
113
114 type Part = (Handle<Mesh>, Handle<ColorMaterial>);
115
116 #[derive(Resource, Default)]
117 pub struct StructureAssets {
118 stone: Part,
119 log: Part,
120 flame: Part,
121 flame_core: Part,
122 stick: Part,
123 bed_frame: Part,
124 blanket: Part,
125 pillow: Part,
126 wall: Part,
127 wall_top: Part,
128 door: Part,
129 door_knob: Part,
130 bench: Part,
131 bench_leg: Part,
132 anvil: Part,
133 plinth: Part,
134 gold: Part,
135 }
136
137 const CAMPFIRE_STONES: usize = 7;
138
139 fn flame_mesh(width: f32, height: f32) -> Mesh {
140 Triangle2d::new(
141 Vec2::new(-width / 2.0, -height / 2.0),
142 Vec2::new(width / 2.0, -height / 2.0),
143 Vec2::new(0.0, height / 2.0),
144 )
145 .into()
146 }
147
148 fn create_structure_assets(
149 mut commands: Commands,
150 mut meshes: ResMut<Assets<Mesh>>,
151 mut materials: ResMut<Assets<ColorMaterial>>,
152 ) {
153 let mut part = |mesh: Mesh, color: Color| (meshes.add(mesh), materials.add(color));
154 commands.insert_resource(StructureAssets {
155 stone: part(
156 Circle::new(TILE_SIZE * 0.09).into(),
157 Color::srgb_u8(120, 116, 110),
158 ),
159 log: part(
160 Rectangle::new(TILE_SIZE * 0.7, TILE_SIZE * 0.14).into(),
161 Color::srgb_u8(110, 74, 42),
162 ),
163 flame: part(
164 flame_mesh(TILE_SIZE * 0.36, TILE_SIZE * 0.5),
165 Color::srgb_u8(240, 110, 30),
166 ),
167 flame_core: part(
168 flame_mesh(TILE_SIZE * 0.18, TILE_SIZE * 0.28),
169 Color::srgb_u8(255, 220, 90),
170 ),
171 stick: part(
172 Rectangle::new(TILE_SIZE * 0.12, TILE_SIZE * 0.5).into(),
173 Color::srgb_u8(100, 70, 40),
174 ),
175 bed_frame: part(
176 Rectangle::new(TILE_SIZE * 0.7, TILE_SIZE * 0.92).into(),
177 Color::srgb_u8(120, 82, 48),
178 ),
179 blanket: part(
180 Rectangle::new(TILE_SIZE * 0.6, TILE_SIZE * 0.55).into(),
181 Color::srgb_u8(70, 110, 160),
182 ),
183 pillow: part(
184 Rectangle::new(TILE_SIZE * 0.5, TILE_SIZE * 0.18).into(),
185 Color::srgb_u8(230, 228, 220),
186 ),
187 wall: part(
188 Rectangle::new(TILE_SIZE, TILE_SIZE).into(),
189 Color::srgb_u8(84, 82, 80),
190 ),
191 wall_top: part(
192 Rectangle::new(TILE_SIZE * 0.8, TILE_SIZE * 0.8).into(),
193 Color::srgb_u8(132, 128, 122),
194 ),
195 door: part(
196 Rectangle::new(TILE_SIZE * 0.9, TILE_SIZE * 0.9).into(),
197 Color::srgb_u8(140, 96, 54),
198 ),
199 door_knob: part(
200 Rectangle::new(TILE_SIZE * 0.12, TILE_SIZE * 0.12).into(),
201 Color::srgb_u8(60, 44, 30),
202 ),
203 bench: part(
204 Rectangle::new(TILE_SIZE * 0.9, TILE_SIZE * 0.45).into(),
205 Color::srgb_u8(150, 104, 60),
206 ),
207 bench_leg: part(
208 Rectangle::new(TILE_SIZE * 0.1, TILE_SIZE * 0.35).into(),
209 Color::srgb_u8(96, 66, 38),
210 ),
211 anvil: part(
212 Rectangle::new(TILE_SIZE * 0.32, TILE_SIZE * 0.18).into(),
213 Color::srgb_u8(70, 74, 82),
214 ),
215 plinth: part(
216 Rectangle::new(TILE_SIZE * 0.6, TILE_SIZE * 0.22).into(),
217 Color::srgb_u8(150, 146, 140),
218 ),
219 gold: part(
220 Triangle2d::new(
221 Vec2::new(-TILE_SIZE * 0.2, -TILE_SIZE * 0.25),
222 Vec2::new(TILE_SIZE * 0.2, -TILE_SIZE * 0.25),
223 Vec2::new(0.0, TILE_SIZE * 0.35),
224 )
225 .into(),
226 Color::srgb_u8(236, 196, 60),
227 ),
228 });
229 }
230
231 fn part_bundle((mesh, material): &Part, transform: Transform) -> impl Bundle {
232 (
233 Mesh2d(mesh.clone()),
234 MeshMaterial2d(material.clone()),
235 transform,
236 )
237 }
238
239 pub fn spawn_structure(
240 commands: &mut Commands,
241 assets: &StructureAssets,
242 tile: UVec2,
243 kind: StructureKind,
244 ) -> Entity {
245 let mut entity = commands.spawn((
246 Name::new(format!("{kind:?}")),
247 Structure { kind },
248 TilePos(tile),
249 Transform::from_translation(WorldMap::tile_center(tile).extend(0.8)),
250 ));
251 if let Some(fuel) = kind.fuel_ticks() {
252 entity.insert(Fuel(fuel));
253 }
254 let at = |x: f32, y: f32, z: f32| Transform::from_xyz(x * TILE_SIZE, y * TILE_SIZE, z);
255 match kind {
256 StructureKind::Campfire => entity.with_children(|c| {
257 for i in 0..CAMPFIRE_STONES {
258 let angle = i as f32 / CAMPFIRE_STONES as f32 * std::f32::consts::TAU;
259 let pos = Vec2::from_angle(angle) * 0.38;
260 c.spawn(part_bundle(&assets.stone, at(pos.x, pos.y, 0.0)));
261 }
262 for angle in [0.6_f32, -0.6] {
263 let log = at(0.0, -0.08, 0.01).with_rotation(Quat::from_rotation_z(angle));
264 c.spawn(part_bundle(&assets.log, log));
265 }
266 c.spawn(part_bundle(&assets.flame, at(0.0, 0.08, 0.02)));
267 c.spawn(part_bundle(&assets.flame_core, at(0.0, 0.03, 0.03)));
268 }),
269 StructureKind::Bed => entity.with_children(|c| {
270 c.spawn(part_bundle(&assets.bed_frame, at(0.0, 0.0, 0.0)));
271 c.spawn(part_bundle(&assets.blanket, at(0.0, -0.13, 0.01)));
272 c.spawn(part_bundle(&assets.pillow, at(0.0, 0.3, 0.01)));
273 }),
274 StructureKind::Wall => entity.with_children(|c| {
275 c.spawn(part_bundle(&assets.wall, at(0.0, 0.0, 0.0)));
276 c.spawn(part_bundle(&assets.wall_top, at(0.0, 0.0, 0.01)));
277 }),
278 StructureKind::Door => entity.with_children(|c| {
279 c.spawn(part_bundle(&assets.door, at(0.0, 0.0, 0.0)));
280 c.spawn(part_bundle(&assets.door_knob, at(0.25, 0.0, 0.01)));
281 }),
282 StructureKind::Workbench => entity.with_children(|c| {
283 for x in [-0.35, 0.35] {
284 c.spawn(part_bundle(&assets.bench_leg, at(x, -0.25, 0.0)));
285 }
286 c.spawn(part_bundle(&assets.bench, at(0.0, 0.05, 0.01)));
287 c.spawn(part_bundle(&assets.anvil, at(0.15, 0.2, 0.02)));
288 }),
289 StructureKind::Statue => entity.with_children(|c| {
290 c.spawn(part_bundle(&assets.plinth, at(0.0, -0.35, 0.0)));
291 c.spawn(part_bundle(&assets.gold, at(0.0, 0.05, 0.01)));
292 }),
293 StructureKind::Torch => entity.with_children(|c| {
294 c.spawn(part_bundle(&assets.stick, at(0.0, -0.15, 0.0)));
295 let small = Vec3::splat(0.6);
296 c.spawn(part_bundle(
297 &assets.flame,
298 at(0.0, 0.18, 0.01).with_scale(small),
299 ));
300 c.spawn(part_bundle(
301 &assets.flame_core,
302 at(0.0, 0.15, 0.02).with_scale(small),
303 ));
304 }),
305 };
306 entity.id()
307 }
308
309 #[cfg(test)]
310 mod tests {
311 use super::*;
312
313 #[test]
314 fn fires_burn_out() {
315 let mut app = App::new();
316 app.add_systems(Update, burn_fuel);
317 let fire = app
318 .world_mut()
319 .spawn((
320 Structure {
321 kind: StructureKind::Torch,
322 },
323 Fuel(3),
324 ))
325 .id();
326 for _ in 0..3 {
327 app.update();
328 }
329 assert!(app.world().get_entity(fire).is_err());
330 }
331
332 #[test]
333 fn beds_are_permanent() {
334 assert_eq!(StructureKind::Bed.fuel_ticks(), None);
335 assert!(StructureKind::Campfire.fuel_ticks().is_some());
336 }
337 }