use bevy::prelude::*; use crate::{ sim::{SimSystems, TICKS_PER_HOUR}, world::{TILE_SIZE, TilePos, WorldMap}, worldgen::WorldGenerated, }; pub struct StructuresPlugin; impl Plugin for StructuresPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, create_structure_assets) .add_systems( Update, clear_structures.run_if(on_message::), ) .add_systems(FixedUpdate, burn_fuel.in_set(SimSystems::Act)); } } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum StructureKind { Campfire, Torch, Bed, } impl StructureKind { pub fn wood_cost(self) -> u32 { match self { StructureKind::Campfire => 10, StructureKind::Torch => 2, StructureKind::Bed => 15, } } pub fn fuel_ticks(self) -> Option { let hours = match self { StructureKind::Campfire => 12, StructureKind::Torch => 8, StructureKind::Bed => return None, }; Some(hours * TICKS_PER_HOUR as u32) } pub fn light(self) -> Option<(f32, Vec3)> { match self { StructureKind::Campfire => Some((7.0, Vec3::new(1.1, 0.8, 0.5))), StructureKind::Torch => Some((4.0, Vec3::new(1.0, 0.8, 0.55))), StructureKind::Bed => None, } } pub fn radius(self) -> f32 { self.light().map_or(0.0, |(radius, _)| radius) } pub fn name(self) -> &'static str { match self { StructureKind::Campfire => "campfire", StructureKind::Torch => "torch", StructureKind::Bed => "bed", } } } #[derive(Component, Debug, Clone, Copy, PartialEq)] #[require(Transform, Visibility)] pub struct Structure { pub kind: StructureKind, } #[derive(Component, Debug, Clone, Copy, PartialEq)] pub struct Fuel(pub u32); fn burn_fuel(mut commands: Commands, mut fuel: Query<(Entity, &mut Fuel)>) { for (entity, mut fuel) in &mut fuel { fuel.0 = fuel.0.saturating_sub(1); if fuel.0 == 0 { commands.entity(entity).despawn(); } } } fn clear_structures(mut commands: Commands, structures: Query>) { for entity in &structures { commands.entity(entity).despawn(); } } type Part = (Handle, Handle); #[derive(Resource, Default)] pub struct StructureAssets { stone: Part, log: Part, flame: Part, flame_core: Part, stick: Part, bed_frame: Part, blanket: Part, pillow: Part, } const CAMPFIRE_STONES: usize = 7; fn flame_mesh(width: f32, height: f32) -> Mesh { Triangle2d::new( Vec2::new(-width / 2.0, -height / 2.0), Vec2::new(width / 2.0, -height / 2.0), Vec2::new(0.0, height / 2.0), ) .into() } fn create_structure_assets( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, ) { let mut part = |mesh: Mesh, color: Color| (meshes.add(mesh), materials.add(color)); commands.insert_resource(StructureAssets { stone: part( Circle::new(TILE_SIZE * 0.09).into(), Color::srgb_u8(120, 116, 110), ), log: part( Rectangle::new(TILE_SIZE * 0.7, TILE_SIZE * 0.14).into(), Color::srgb_u8(110, 74, 42), ), flame: part( flame_mesh(TILE_SIZE * 0.36, TILE_SIZE * 0.5), Color::srgb_u8(240, 110, 30), ), flame_core: part( flame_mesh(TILE_SIZE * 0.18, TILE_SIZE * 0.28), Color::srgb_u8(255, 220, 90), ), stick: part( Rectangle::new(TILE_SIZE * 0.12, TILE_SIZE * 0.5).into(), Color::srgb_u8(100, 70, 40), ), bed_frame: part( Rectangle::new(TILE_SIZE * 0.7, TILE_SIZE * 0.92).into(), Color::srgb_u8(120, 82, 48), ), blanket: part( Rectangle::new(TILE_SIZE * 0.6, TILE_SIZE * 0.55).into(), Color::srgb_u8(70, 110, 160), ), pillow: part( Rectangle::new(TILE_SIZE * 0.5, TILE_SIZE * 0.18).into(), Color::srgb_u8(230, 228, 220), ), }); } fn part_bundle((mesh, material): &Part, transform: Transform) -> impl Bundle { ( Mesh2d(mesh.clone()), MeshMaterial2d(material.clone()), transform, ) } pub fn spawn_structure( commands: &mut Commands, assets: &StructureAssets, tile: UVec2, kind: StructureKind, ) { let mut entity = commands.spawn(( Name::new(format!("{kind:?}")), Structure { kind }, TilePos(tile), Transform::from_translation(WorldMap::tile_center(tile).extend(0.8)), )); if let Some(fuel) = kind.fuel_ticks() { entity.insert(Fuel(fuel)); } let at = |x: f32, y: f32, z: f32| Transform::from_xyz(x * TILE_SIZE, y * TILE_SIZE, z); match kind { StructureKind::Campfire => entity.with_children(|c| { for i in 0..CAMPFIRE_STONES { let angle = i as f32 / CAMPFIRE_STONES as f32 * std::f32::consts::TAU; let pos = Vec2::from_angle(angle) * 0.38; c.spawn(part_bundle(&assets.stone, at(pos.x, pos.y, 0.0))); } for angle in [0.6_f32, -0.6] { let log = at(0.0, -0.08, 0.01).with_rotation(Quat::from_rotation_z(angle)); c.spawn(part_bundle(&assets.log, log)); } c.spawn(part_bundle(&assets.flame, at(0.0, 0.08, 0.02))); c.spawn(part_bundle(&assets.flame_core, at(0.0, 0.03, 0.03))); }), StructureKind::Bed => entity.with_children(|c| { c.spawn(part_bundle(&assets.bed_frame, at(0.0, 0.0, 0.0))); c.spawn(part_bundle(&assets.blanket, at(0.0, -0.13, 0.01))); c.spawn(part_bundle(&assets.pillow, at(0.0, 0.3, 0.01))); }), StructureKind::Torch => entity.with_children(|c| { c.spawn(part_bundle(&assets.stick, at(0.0, -0.15, 0.0))); let small = Vec3::splat(0.6); c.spawn(part_bundle( &assets.flame, at(0.0, 0.18, 0.01).with_scale(small), )); c.spawn(part_bundle( &assets.flame_core, at(0.0, 0.15, 0.02).with_scale(small), )); }), }; } #[cfg(test)] mod tests { use super::*; #[test] fn fires_burn_out() { let mut app = App::new(); app.add_systems(Update, burn_fuel); let fire = app .world_mut() .spawn(( Structure { kind: StructureKind::Torch, }, Fuel(3), )) .id(); for _ in 0..3 { app.update(); } assert!(app.world().get_entity(fire).is_err()); } #[test] fn beds_are_permanent() { assert_eq!(StructureKind::Bed.fuel_ticks(), None); assert!(StructureKind::Campfire.fuel_ticks().is_some()); } }