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