src/structures.rs 7 K · 245 lines · raw · history

1 use bevy::prelude::*;
2
3 use crate::{
4 sim::{SimSystems, TICKS_PER_HOUR},
5 world::{TILE_SIZE, TilePos, WorldMap},
6 worldgen::WorldGenerated,
7 };
8
9 pub struct StructuresPlugin;
10
11 impl Plugin for StructuresPlugin {
12 fn build(&self, app: &mut App) {
13 app.add_systems(Startup, create_structure_assets)
14 .add_systems(
15 Update,
16 clear_structures.run_if(on_message::<WorldGenerated>),
17 )
18 .add_systems(FixedUpdate, burn_fuel.in_set(SimSystems::Act));
19 }
20 }
21
22 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
23 pub enum StructureKind {
24 Campfire,
25 Torch,
26 Bed,
27 }
28
29 impl StructureKind {
30 pub fn wood_cost(self) -> u32 {
31 match self {
32 StructureKind::Campfire => 10,
33 StructureKind::Torch => 2,
34 StructureKind::Bed => 15,
35 }
36 }
37
38 pub fn fuel_ticks(self) -> Option<u32> {
39 let hours = match self {
40 StructureKind::Campfire => 12,
41 StructureKind::Torch => 8,
42 StructureKind::Bed => return None,
43 };
44 Some(hours * TICKS_PER_HOUR as u32)
45 }
46
47 pub fn light(self) -> Option<(f32, Vec3)> {
48 match self {
49 StructureKind::Campfire => Some((7.0, Vec3::new(1.1, 0.8, 0.5))),
50 StructureKind::Torch => Some((4.0, Vec3::new(1.0, 0.8, 0.55))),
51 StructureKind::Bed => None,
52 }
53 }
54
55 pub fn radius(self) -> f32 {
56 self.light().map_or(0.0, |(radius, _)| radius)
57 }
58
59 pub fn name(self) -> &'static str {
60 match self {
61 StructureKind::Campfire => "campfire",
62 StructureKind::Torch => "torch",
63 StructureKind::Bed => "bed",
64 }
65 }
66 }
67
68 #[derive(Component, Debug, Clone, Copy, PartialEq)]
69 #[require(Transform, Visibility)]
70 pub struct Structure {
71 pub kind: StructureKind,
72 }
73
74 #[derive(Component, Debug, Clone, Copy, PartialEq)]
75 pub struct Fuel(pub u32);
76
77 fn burn_fuel(mut commands: Commands, mut fuel: Query<(Entity, &mut Fuel)>) {
78 for (entity, mut fuel) in &mut fuel {
79 fuel.0 = fuel.0.saturating_sub(1);
80 if fuel.0 == 0 {
81 commands.entity(entity).despawn();
82 }
83 }
84 }
85
86 fn clear_structures(mut commands: Commands, structures: Query<Entity, With<Structure>>) {
87 for entity in &structures {
88 commands.entity(entity).despawn();
89 }
90 }
91
92 type Part = (Handle<Mesh>, Handle<ColorMaterial>);
93
94 #[derive(Resource, Default)]
95 pub struct StructureAssets {
96 stone: Part,
97 log: Part,
98 flame: Part,
99 flame_core: Part,
100 stick: Part,
101 bed_frame: Part,
102 blanket: Part,
103 pillow: Part,
104 }
105
106 const CAMPFIRE_STONES: usize = 7;
107
108 fn flame_mesh(width: f32, height: f32) -> Mesh {
109 Triangle2d::new(
110 Vec2::new(-width / 2.0, -height / 2.0),
111 Vec2::new(width / 2.0, -height / 2.0),
112 Vec2::new(0.0, height / 2.0),
113 )
114 .into()
115 }
116
117 fn create_structure_assets(
118 mut commands: Commands,
119 mut meshes: ResMut<Assets<Mesh>>,
120 mut materials: ResMut<Assets<ColorMaterial>>,
121 ) {
122 let mut part = |mesh: Mesh, color: Color| (meshes.add(mesh), materials.add(color));
123 commands.insert_resource(StructureAssets {
124 stone: part(
125 Circle::new(TILE_SIZE * 0.09).into(),
126 Color::srgb_u8(120, 116, 110),
127 ),
128 log: part(
129 Rectangle::new(TILE_SIZE * 0.7, TILE_SIZE * 0.14).into(),
130 Color::srgb_u8(110, 74, 42),
131 ),
132 flame: part(
133 flame_mesh(TILE_SIZE * 0.36, TILE_SIZE * 0.5),
134 Color::srgb_u8(240, 110, 30),
135 ),
136 flame_core: part(
137 flame_mesh(TILE_SIZE * 0.18, TILE_SIZE * 0.28),
138 Color::srgb_u8(255, 220, 90),
139 ),
140 stick: part(
141 Rectangle::new(TILE_SIZE * 0.12, TILE_SIZE * 0.5).into(),
142 Color::srgb_u8(100, 70, 40),
143 ),
144 bed_frame: part(
145 Rectangle::new(TILE_SIZE * 0.7, TILE_SIZE * 0.92).into(),
146 Color::srgb_u8(120, 82, 48),
147 ),
148 blanket: part(
149 Rectangle::new(TILE_SIZE * 0.6, TILE_SIZE * 0.55).into(),
150 Color::srgb_u8(70, 110, 160),
151 ),
152 pillow: part(
153 Rectangle::new(TILE_SIZE * 0.5, TILE_SIZE * 0.18).into(),
154 Color::srgb_u8(230, 228, 220),
155 ),
156 });
157 }
158
159 fn part_bundle((mesh, material): &Part, transform: Transform) -> impl Bundle {
160 (
161 Mesh2d(mesh.clone()),
162 MeshMaterial2d(material.clone()),
163 transform,
164 )
165 }
166
167 pub fn spawn_structure(
168 commands: &mut Commands,
169 assets: &StructureAssets,
170 tile: UVec2,
171 kind: StructureKind,
172 ) {
173 let mut entity = commands.spawn((
174 Name::new(format!("{kind:?}")),
175 Structure { kind },
176 TilePos(tile),
177 Transform::from_translation(WorldMap::tile_center(tile).extend(0.8)),
178 ));
179 if let Some(fuel) = kind.fuel_ticks() {
180 entity.insert(Fuel(fuel));
181 }
182 let at = |x: f32, y: f32, z: f32| Transform::from_xyz(x * TILE_SIZE, y * TILE_SIZE, z);
183 match kind {
184 StructureKind::Campfire => entity.with_children(|c| {
185 for i in 0..CAMPFIRE_STONES {
186 let angle = i as f32 / CAMPFIRE_STONES as f32 * std::f32::consts::TAU;
187 let pos = Vec2::from_angle(angle) * 0.38;
188 c.spawn(part_bundle(&assets.stone, at(pos.x, pos.y, 0.0)));
189 }
190 for angle in [0.6_f32, -0.6] {
191 let log = at(0.0, -0.08, 0.01).with_rotation(Quat::from_rotation_z(angle));
192 c.spawn(part_bundle(&assets.log, log));
193 }
194 c.spawn(part_bundle(&assets.flame, at(0.0, 0.08, 0.02)));
195 c.spawn(part_bundle(&assets.flame_core, at(0.0, 0.03, 0.03)));
196 }),
197 StructureKind::Bed => entity.with_children(|c| {
198 c.spawn(part_bundle(&assets.bed_frame, at(0.0, 0.0, 0.0)));
199 c.spawn(part_bundle(&assets.blanket, at(0.0, -0.13, 0.01)));
200 c.spawn(part_bundle(&assets.pillow, at(0.0, 0.3, 0.01)));
201 }),
202 StructureKind::Torch => entity.with_children(|c| {
203 c.spawn(part_bundle(&assets.stick, at(0.0, -0.15, 0.0)));
204 let small = Vec3::splat(0.6);
205 c.spawn(part_bundle(
206 &assets.flame,
207 at(0.0, 0.18, 0.01).with_scale(small),
208 ));
209 c.spawn(part_bundle(
210 &assets.flame_core,
211 at(0.0, 0.15, 0.02).with_scale(small),
212 ));
213 }),
214 };
215 }
216
217 #[cfg(test)]
218 mod tests {
219 use super::*;
220
221 #[test]
222 fn fires_burn_out() {
223 let mut app = App::new();
224 app.add_systems(Update, burn_fuel);
225 let fire = app
226 .world_mut()
227 .spawn((
228 Structure {
229 kind: StructureKind::Torch,
230 },
231 Fuel(3),
232 ))
233 .id();
234 for _ in 0..3 {
235 app.update();
236 }
237 assert!(app.world().get_entity(fire).is_err());
238 }
239
240 #[test]
241 fn beds_are_permanent() {
242 assert_eq!(StructureKind::Bed.fuel_ticks(), None);
243 assert!(StructureKind::Campfire.fuel_ticks().is_some());
244 }
245 }