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