src/structures.rs 8.6 K · 288 lines · raw · history

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