Add workbench, iron tools and gold statues
- author
- Marijn Besseling <njirambem@gmail.com> · 2026-10-25 06:31 UTC
- commit
- 4b47c06399a95160e2533c91fed37a6935165fdb
- parent
- 74cb0e7c3d
- tree
- browse at this commit
9 files changed +400 -51
| src/building.rs | +5 -1 | |
| src/economy.rs | +12 -4 | |
| src/family.rs | +55 -1 | |
| src/items.rs | +4 -1 | |
| src/jobs/mod.rs | +188 -30 | |
| src/jobs/tests.rs | +45 -0 | |
| src/mood.rs | +16 -3 | |
| src/selection.rs | +25 -8 | |
| src/structures.rs | +50 -3 |
src/building.rs +5 -1
| @@ -76,6 +76,8 @@ impl BuildKind { | ||
| 76 | 76 | BuildKind::Structure(StructureKind::Bed) => 600, |
| 77 | 77 | BuildKind::Structure(StructureKind::Wall) => 300, |
| 78 | 78 | BuildKind::Structure(StructureKind::Door) => 300, |
| 79 | + BuildKind::Structure(StructureKind::Workbench) => 600, | |
| 80 | + BuildKind::Structure(StructureKind::Statue) => 900, | |
| 79 | 81 | BuildKind::Roof => 120, |
| 80 | 82 | BuildKind::Pave => 120, |
| 81 | 83 | BuildKind::Refuel(_) => 60, |
| @@ -305,12 +307,14 @@ pub enum BuildMode { | ||
| 305 | 307 | Deconstruct, |
| 306 | 308 | } |
| 307 | 309 | |
| 308 | -pub const BUILD_TOOL_CYCLE: [BuildMode; 6] = [ | |
| 310 | +pub const BUILD_TOOL_CYCLE: [BuildMode; 8] = [ | |
| 309 | 311 | BuildMode::Place(StructureKind::Wall), |
| 310 | 312 | BuildMode::Place(StructureKind::Door), |
| 311 | 313 | BuildMode::Place(StructureKind::Campfire), |
| 312 | 314 | BuildMode::Place(StructureKind::Torch), |
| 313 | 315 | BuildMode::Place(StructureKind::Bed), |
| 316 | + BuildMode::Place(StructureKind::Workbench), | |
| 317 | + BuildMode::Place(StructureKind::Statue), | |
| 314 | 318 | BuildMode::Deconstruct, |
| 315 | 319 | ]; |
| 316 | 320 | |
src/economy.rs +12 -4
| @@ -30,16 +30,18 @@ pub enum Stock { | ||
| 30 | 30 | Gold, |
| 31 | 31 | RawFood, |
| 32 | 32 | Meals, |
| 33 | + Tools, | |
| 33 | 34 | } |
| 34 | 35 | |
| 35 | 36 | impl Stock { |
| 36 | - pub const ALL: [Stock; 6] = [ | |
| 37 | + pub const ALL: [Stock; 7] = [ | |
| 37 | 38 | Stock::Wood, |
| 38 | 39 | Stock::Stone, |
| 39 | 40 | Stock::Iron, |
| 40 | 41 | Stock::Gold, |
| 41 | 42 | Stock::RawFood, |
| 42 | 43 | Stock::Meals, |
| 44 | + Stock::Tools, | |
| 43 | 45 | ]; |
| 44 | 46 | |
| 45 | 47 | pub fn of(kind: ItemKind) -> Stock { |
| @@ -52,6 +54,7 @@ impl Stock { | ||
| 52 | 54 | Stock::RawFood |
| 53 | 55 | } |
| 54 | 56 | ItemKind::Meal => Stock::Meals, |
| 57 | + ItemKind::Tools => Stock::Tools, | |
| 55 | 58 | } |
| 56 | 59 | } |
| 57 | 60 | |
| @@ -73,6 +76,7 @@ impl Stock { | ||
| 73 | 76 | Stock::Gold => "Gold", |
| 74 | 77 | Stock::RawFood => "Raw food", |
| 75 | 78 | Stock::Meals => "Meals", |
| 79 | + Stock::Tools => "Tools", | |
| 76 | 80 | } |
| 77 | 81 | } |
| 78 | 82 | |
| @@ -88,9 +92,11 @@ impl Stock { | ||
| 88 | 92 | match self { |
| 89 | 93 | Stock::Wood => 15.0, |
| 90 | 94 | Stock::Stone => 5.0, |
| 91 | - Stock::Iron | Stock::Gold => 0.0, | |
| 95 | + Stock::Iron => 6.0, | |
| 96 | + Stock::Gold => 0.0, | |
| 92 | 97 | Stock::RawFood => 10.0, |
| 93 | 98 | Stock::Meals => 3.0, |
| 99 | + Stock::Tools => 1.0, | |
| 94 | 100 | } |
| 95 | 101 | } |
| 96 | 102 | |
| @@ -98,9 +104,11 @@ impl Stock { | ||
| 98 | 104 | match self { |
| 99 | 105 | Stock::Wood => 20.0, |
| 100 | 106 | Stock::Stone => 10.0, |
| 101 | - Stock::Iron | Stock::Gold => 0.0, | |
| 107 | + Stock::Iron => 3.0, | |
| 108 | + Stock::Gold => 0.0, | |
| 102 | 109 | Stock::RawFood => 30.0, |
| 103 | 110 | Stock::Meals => 3.0, |
| 111 | + Stock::Tools => 1.0, | |
| 104 | 112 | } |
| 105 | 113 | } |
| 106 | 114 | } |
| @@ -288,7 +296,7 @@ mod tests { | ||
| 288 | 296 | let mut goals = ColonyGoals::default(); |
| 289 | 297 | learn_days(&mut goals, ItemKind::Wood, 0, 10); |
| 290 | 298 | assert_eq!(goals.goal(Stock::Wood), 15); |
| 291 | - assert_eq!(goals.goal(Stock::Iron), 0); | |
| 299 | + assert_eq!(goals.goal(Stock::Gold), 0); | |
| 292 | 300 | } |
| 293 | 301 | |
| 294 | 302 | // Raw foods stand in for each other (cooking accepts any), so the colony targets them as one |
src/family.rs +55 -1
| @@ -103,6 +103,10 @@ impl Home { | ||
| 103 | 103 | [corner, corner + UVec2::X] |
| 104 | 104 | } |
| 105 | 105 | |
| 106 | + pub fn statue_tile(&self) -> UVec2 { | |
| 107 | + UVec2::new(self.hi.x - 1, self.lo.y + 1) | |
| 108 | + } | |
| 109 | + | |
| 106 | 110 | pub fn blueprints(&self, toward: UVec2) -> HashMap<UVec2, BuildKind> { |
| 107 | 111 | let corner = |t: &UVec2| { |
| 108 | 112 | (t.x == self.lo.x || t.x == self.hi.x) && (t.y == self.lo.y || t.y == self.hi.y) |
| @@ -352,13 +356,24 @@ fn plan_homes( | ||
| 352 | 356 | .filter(|(_, s)| s.kind == StructureKind::Door) |
| 353 | 357 | .map(|(pos, _)| pos.0) |
| 354 | 358 | .collect(); |
| 359 | + let statues: HashSet<UVec2> = structures | |
| 360 | + .iter() | |
| 361 | + .filter(|(_, s)| s.kind == StructureKind::Statue) | |
| 362 | + .map(|(pos, _)| pos.0) | |
| 363 | + .collect(); | |
| 355 | 364 | for home in homes.by_family.values() { |
| 356 | 365 | if blueprints.0.keys().any(|t| home.contains(*t)) { |
| 357 | 366 | continue; |
| 358 | 367 | } |
| 359 | - for tile in roofs_needed(&map, &doors, home) { | |
| 368 | + let roofs = roofs_needed(&map, &doors, home); | |
| 369 | + let built = roofs.is_empty() && home.interior().all(|t| map.is_roofed(t.as_ivec2())); | |
| 370 | + for tile in roofs { | |
| 360 | 371 | blueprints.0.insert(tile, BuildKind::Roof); |
| 361 | 372 | } |
| 373 | + if built && !statues.iter().any(|&t| home.contains(t)) { | |
| 374 | + let statue = BuildKind::Structure(StructureKind::Statue); | |
| 375 | + blueprints.0.insert(home.statue_tile(), statue); | |
| 376 | + } | |
| 362 | 377 | } |
| 363 | 378 | let Some(family) = families |
| 364 | 379 | .iter() |
| @@ -625,4 +640,43 @@ mod tests { | ||
| 625 | 640 | let roofs = roofs_needed(&map, &HashSet::from([door]), &home); |
| 626 | 641 | assert_eq!(roofs.len(), (HOME_INTERIOR.x * HOME_INTERIOR.y) as usize); |
| 627 | 642 | } |
| 643 | + | |
| 644 | + // Gold has one use: a statue in each finished home, which lifts the family's mood. Planning | |
| 645 | + // it is what makes the colony want gold at all. | |
| 646 | + #[test] | |
| 647 | + fn finished_homes_get_a_statue() { | |
| 648 | + let mut map = WorldMap::filled(UVec2::splat(20), Terrain::Grass); | |
| 649 | + let home = Home::at(UVec2::new(5, 5)); | |
| 650 | + let door = UVec2::new(7, 5); | |
| 651 | + for tile in home.ring().filter(|t| *t != door) { | |
| 652 | + map.set_wall(tile, true); | |
| 653 | + } | |
| 654 | + for tile in home.interior() { | |
| 655 | + map.set_roof(tile, true); | |
| 656 | + } | |
| 657 | + let mut app = App::new(); | |
| 658 | + app.insert_resource(map) | |
| 659 | + .init_resource::<SimTick>() | |
| 660 | + .init_resource::<Zones>() | |
| 661 | + .init_resource::<Blueprints>() | |
| 662 | + .insert_resource(Homes { | |
| 663 | + by_family: HashMap::from([(1, home)]), | |
| 664 | + ..default() | |
| 665 | + }) | |
| 666 | + .add_systems(Update, plan_homes); | |
| 667 | + app.world_mut().spawn(( | |
| 668 | + Structure { | |
| 669 | + kind: StructureKind::Door, | |
| 670 | + }, | |
| 671 | + TilePos(door), | |
| 672 | + )); | |
| 673 | + app.world_mut() | |
| 674 | + .spawn((Colonist { name: "Ada".into() }, Family(1))); | |
| 675 | + app.update(); | |
| 676 | + let blueprints = app.world().resource::<Blueprints>(); | |
| 677 | + assert_eq!( | |
| 678 | + blueprints.get(home.statue_tile()), | |
| 679 | + Some(BuildKind::Structure(StructureKind::Statue)) | |
| 680 | + ); | |
| 681 | + } | |
| 628 | 682 | } |
src/items.rs +4 -1
| @@ -30,10 +30,11 @@ pub enum ItemKind { | ||
| 30 | 30 | Vegetables, |
| 31 | 31 | Meal, |
| 32 | 32 | Carcass, |
| 33 | + Tools, | |
| 33 | 34 | } |
| 34 | 35 | |
| 35 | 36 | impl ItemKind { |
| 36 | - pub const ALL: [ItemKind; 9] = [ | |
| 37 | + pub const ALL: [ItemKind; 10] = [ | |
| 37 | 38 | ItemKind::Wood, |
| 38 | 39 | ItemKind::Stone, |
| 39 | 40 | ItemKind::Iron, |
| @@ -43,6 +44,7 @@ impl ItemKind { | ||
| 43 | 44 | ItemKind::Vegetables, |
| 44 | 45 | ItemKind::Meal, |
| 45 | 46 | ItemKind::Carcass, |
| 47 | + ItemKind::Tools, | |
| 46 | 48 | ]; |
| 47 | 49 | |
| 48 | 50 | pub fn nutrition(self) -> f32 { |
| @@ -72,6 +74,7 @@ impl ItemKind { | ||
| 72 | 74 | ItemKind::Vegetables => Color::srgb_u8(120, 170, 60), |
| 73 | 75 | ItemKind::Meal => Color::srgb_u8(240, 210, 150), |
| 74 | 76 | ItemKind::Carcass => Color::srgb_u8(120, 60, 50), |
| 77 | + ItemKind::Tools => Color::srgb_u8(90, 100, 120), | |
| 75 | 78 | } |
| 76 | 79 | } |
| 77 | 80 | } |
src/jobs/mod.rs +188 -30
| @@ -89,15 +89,17 @@ pub enum WorkType { | ||
| 89 | 89 | Gather, |
| 90 | 90 | Haul, |
| 91 | 91 | Tame, |
| 92 | + Craft, | |
| 92 | 93 | } |
| 93 | 94 | |
| 94 | 95 | impl WorkType { |
| 95 | - pub const ALL: [WorkType; 5] = [ | |
| 96 | + pub const ALL: [WorkType; 6] = [ | |
| 96 | 97 | WorkType::Food, |
| 97 | 98 | WorkType::Build, |
| 98 | 99 | WorkType::Gather, |
| 99 | 100 | WorkType::Haul, |
| 100 | 101 | WorkType::Tame, |
| 102 | + WorkType::Craft, | |
| 101 | 103 | ]; |
| 102 | 104 | |
| 103 | 105 | pub fn label(self) -> &'static str { |
| @@ -107,6 +109,7 @@ impl WorkType { | ||
| 107 | 109 | WorkType::Gather => "Gather", |
| 108 | 110 | WorkType::Haul => "Haul", |
| 109 | 111 | WorkType::Tame => "Tame", |
| 112 | + WorkType::Craft => "Craft", | |
| 110 | 113 | } |
| 111 | 114 | } |
| 112 | 115 | } |
| @@ -173,11 +176,15 @@ pub enum Task { | ||
| 173 | 176 | kind: AnimalKind, |
| 174 | 177 | progress: u32, |
| 175 | 178 | }, |
| 176 | - Cook { | |
| 179 | + Craft { | |
| 180 | + recipe: Recipe, | |
| 177 | 181 | pickup: Option<UVec2>, |
| 178 | 182 | station: UVec2, |
| 179 | 183 | progress: u32, |
| 180 | 184 | }, |
| 185 | + Equip { | |
| 186 | + pickup: UVec2, | |
| 187 | + }, | |
| 181 | 188 | Butcher { |
| 182 | 189 | site: UVec2, |
| 183 | 190 | progress: u32, |
| @@ -189,6 +196,62 @@ pub enum Task { | ||
| 189 | 196 | }, |
| 190 | 197 | } |
| 191 | 198 | |
| 199 | +#[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| 200 | +pub enum Recipe { | |
| 201 | + Meal, | |
| 202 | + Tools, | |
| 203 | +} | |
| 204 | + | |
| 205 | +impl Recipe { | |
| 206 | + pub fn station(self) -> StructureKind { | |
| 207 | + match self { | |
| 208 | + Recipe::Meal => StructureKind::Campfire, | |
| 209 | + Recipe::Tools => StructureKind::Workbench, | |
| 210 | + } | |
| 211 | + } | |
| 212 | + | |
| 213 | + pub fn accepts(self, kind: ItemKind) -> bool { | |
| 214 | + match self { | |
| 215 | + Recipe::Meal => kind.is_raw_food(), | |
| 216 | + Recipe::Tools => kind == ItemKind::Iron, | |
| 217 | + } | |
| 218 | + } | |
| 219 | + | |
| 220 | + pub fn input(self) -> u32 { | |
| 221 | + match self { | |
| 222 | + Recipe::Meal => COOK_INPUT, | |
| 223 | + Recipe::Tools => TOOL_IRON, | |
| 224 | + } | |
| 225 | + } | |
| 226 | + | |
| 227 | + pub fn work(self) -> u32 { | |
| 228 | + match self { | |
| 229 | + Recipe::Meal => COOK_WORK, | |
| 230 | + Recipe::Tools => TOOL_WORK, | |
| 231 | + } | |
| 232 | + } | |
| 233 | + | |
| 234 | + pub fn output(self) -> ItemKind { | |
| 235 | + match self { | |
| 236 | + Recipe::Meal => ItemKind::Meal, | |
| 237 | + Recipe::Tools => ItemKind::Tools, | |
| 238 | + } | |
| 239 | + } | |
| 240 | +} | |
| 241 | + | |
| 242 | +pub const TOOL_IRON: u32 = 3; | |
| 243 | +pub const TOOL_WORK: u32 = 600; | |
| 244 | +pub const TOOL_LIFE_TICKS: u32 = 2 * 24 * crate::sim::TICKS_PER_HOUR as u32; | |
| 245 | + | |
| 246 | +#[derive(Component, Debug, Default, Clone, Copy, PartialEq, Eq)] | |
| 247 | +pub struct Tool { | |
| 248 | + pub wear: u32, | |
| 249 | +} | |
| 250 | + | |
| 251 | +pub fn tool_rate(rate: u32, tool: bool) -> u32 { | |
| 252 | + if tool { (rate * 3).div_ceil(2) } else { rate } | |
| 253 | +} | |
| 254 | + | |
| 192 | 255 | pub fn work_rate(light: &LightMap, tile: UVec2) -> u32 { |
| 193 | 256 | if light.is_lit(tile) { |
| 194 | 257 | LIT_WORK_RATE |
| @@ -416,7 +479,8 @@ fn reserve_task(reservations: &mut Reservations, task: Task, by: Entity) { | ||
| 416 | 479 | reservations.reserve(Target::Animal(animal), by) |
| 417 | 480 | } |
| 418 | 481 | Task::Butcher { site, .. } => reservations.reserve(Target::Pickup(site), by), |
| 419 | - Task::Cook { | |
| 482 | + Task::Equip { pickup } => reservations.reserve(Target::Pickup(pickup), by), | |
| 483 | + Task::Craft { | |
| 420 | 484 | pickup, station, .. |
| 421 | 485 | } => { |
| 422 | 486 | reservations.reserve(Target::Station(station), by); |
| @@ -833,13 +897,13 @@ pub fn food_to_fetch(reservations: &Reservations, food_tiles: &HashSet<UVec2>) - | ||
| 833 | 897 | .any(|&p| reservations.is_free(Target::Pickup(p))) |
| 834 | 898 | } |
| 835 | 899 | |
| 836 | -pub fn raw_food_source(ctx: &BuildContext, from: UVec2) -> Option<UVec2> { | |
| 900 | +pub fn input_source(ctx: &BuildContext, from: UVec2, recipe: Recipe) -> Option<UVec2> { | |
| 837 | 901 | let sources: HashSet<UVec2> = ctx |
| 838 | 902 | .items |
| 839 | 903 | .iter() |
| 840 | 904 | .filter(|(item, pos)| { |
| 841 | - item.kind.is_raw_food() | |
| 842 | - && item.count >= COOK_INPUT | |
| 905 | + recipe.accepts(item.kind) | |
| 906 | + && item.count >= recipe.input() | |
| 843 | 907 | && ctx.reservations.is_free(Target::Pickup(pos.0)) |
| 844 | 908 | }) |
| 845 | 909 | .map(|(_, pos)| pos.0) |
| @@ -866,12 +930,40 @@ pub fn find_butchering( | ||
| 866 | 930 | Some(Task::Butcher { site, progress: 0 }) |
| 867 | 931 | } |
| 868 | 932 | |
| 869 | -pub fn find_cooking(ctx: &BuildContext, from: UVec2) -> Option<Task> { | |
| 933 | +pub fn find_workbench(ctx: &BuildContext, from: UVec2) -> Option<Task> { | |
| 934 | + if ctx.count(StructureKind::Workbench) > 0 { | |
| 935 | + return None; | |
| 936 | + } | |
| 937 | + let home = ctx.zones.home(from); | |
| 938 | + let site = ctx.site_near(home, HOME_RADIUS * 1.5)?; | |
| 939 | + ctx.build_task(from, site, BuildKind::Structure(StructureKind::Workbench)) | |
| 940 | +} | |
| 941 | + | |
| 942 | +pub fn tool_source( | |
| 943 | + map: &WorldMap, | |
| 944 | + reservations: &Reservations, | |
| 945 | + items: &[(Item, TilePos)], | |
| 946 | + from: UVec2, | |
| 947 | +) -> Option<UVec2> { | |
| 948 | + let tools: HashSet<UVec2> = items | |
| 949 | + .iter() | |
| 950 | + .filter(|(item, pos)| { | |
| 951 | + item.kind == ItemKind::Tools && reservations.is_free(Target::Pickup(pos.0)) | |
| 952 | + }) | |
| 953 | + .map(|(_, pos)| pos.0) | |
| 954 | + .collect(); | |
| 955 | + if tools.is_empty() { | |
| 956 | + return None; | |
| 957 | + } | |
| 958 | + nearest_of(map, from, SEARCH_TILES, &tools) | |
| 959 | +} | |
| 960 | + | |
| 961 | +pub fn find_crafting(ctx: &BuildContext, from: UVec2, recipe: Recipe) -> Option<Task> { | |
| 870 | 962 | let station = ctx |
| 871 | 963 | .structures |
| 872 | 964 | .iter() |
| 873 | 965 | .filter(|&&(pos, kind, fuel)| { |
| 874 | - kind == StructureKind::Campfire | |
| 966 | + kind == recipe.station() | |
| 875 | 967 | && fuel >= COOK_MIN_FUEL |
| 876 | 968 | && ctx.reservations.is_free(Target::Station(pos)) |
| 877 | 969 | }) |
| @@ -881,8 +973,9 @@ pub fn find_cooking(ctx: &BuildContext, from: UVec2) -> Option<Task> { | ||
| 881 | 973 | .distance_squared(from.as_vec2()) |
| 882 | 974 | .total_cmp(&b.as_vec2().distance_squared(from.as_vec2())) |
| 883 | 975 | })?; |
| 884 | - let pickup = raw_food_source(ctx, from)?; | |
| 885 | - Some(Task::Cook { | |
| 976 | + let pickup = input_source(ctx, from, recipe)?; | |
| 977 | + Some(Task::Craft { | |
| 978 | + recipe, | |
| 886 | 979 | pickup: Some(pickup), |
| 887 | 980 | station, |
| 888 | 981 | progress: 0, |
| @@ -1024,6 +1117,7 @@ fn choose_jobs( | ||
| 1024 | 1117 | Has<Child>, |
| 1025 | 1118 | &WorkPriorities, |
| 1026 | 1119 | &Warmth, |
| 1120 | + Has<Tool>, | |
| 1027 | 1121 | ), |
| 1028 | 1122 | With<Colonist>, |
| 1029 | 1123 | >, |
| @@ -1083,7 +1177,7 @@ fn choose_jobs( | ||
| 1083 | 1177 | for job in colonists.iter().filter_map(|c| c.3.0) { |
| 1084 | 1178 | match job.task { |
| 1085 | 1179 | Task::Hunt { kind, .. } => *pending.entry(ItemKind::Meat).or_insert(0) += kind.meat(), |
| 1086 | - Task::Cook { .. } => *pending.entry(ItemKind::Meal).or_insert(0) += 1, | |
| 1180 | + Task::Craft { recipe, .. } => *pending.entry(recipe.output()).or_insert(0) += 1, | |
| 1087 | 1181 | _ => {} |
| 1088 | 1182 | } |
| 1089 | 1183 | } |
| @@ -1106,6 +1200,7 @@ fn choose_jobs( | ||
| 1106 | 1200 | child, |
| 1107 | 1201 | priorities, |
| 1108 | 1202 | warmth, |
| 1203 | + has_tool, | |
| 1109 | 1204 | ) in &mut colonists |
| 1110 | 1205 | { |
| 1111 | 1206 | let exhausted = rest.0 < EXHAUSTED_REST; |
| @@ -1177,6 +1272,11 @@ fn choose_jobs( | ||
| 1177 | 1272 | carrying.0 = None; |
| 1178 | 1273 | } |
| 1179 | 1274 | storage.map(|to| Task::Haul { pickup: None, to }) |
| 1275 | + } else if let Some(pickup) = (!has_tool) | |
| 1276 | + .then(|| tool_source(&map, &reservations, &snapshot, tile.0)) | |
| 1277 | + .flatten() | |
| 1278 | + { | |
| 1279 | + Some(Task::Equip { pickup }) | |
| 1180 | 1280 | } else { |
| 1181 | 1281 | let occupied: HashSet<UVec2> = structures_list |
| 1182 | 1282 | .iter() |
| @@ -1195,8 +1295,19 @@ fn choose_jobs( | ||
| 1195 | 1295 | occupied: &occupied, |
| 1196 | 1296 | }; |
| 1197 | 1297 | let cooking = || { |
| 1198 | - (wanted(ItemKind::Meal) && raw_food_source(&build, tile.0).is_some()) | |
| 1199 | - .then(|| find_cooking(&build, tile.0).or_else(|| find_campfire(&build, tile.0))) | |
| 1298 | + (wanted(ItemKind::Meal) && input_source(&build, tile.0, Recipe::Meal).is_some()) | |
| 1299 | + .then(|| { | |
| 1300 | + find_crafting(&build, tile.0, Recipe::Meal) | |
| 1301 | + .or_else(|| find_campfire(&build, tile.0)) | |
| 1302 | + }) | |
| 1303 | + .flatten() | |
| 1304 | + }; | |
| 1305 | + let toolmaking = || { | |
| 1306 | + (wanted(ItemKind::Tools) && input_source(&build, tile.0, Recipe::Tools).is_some()) | |
| 1307 | + .then(|| { | |
| 1308 | + find_crafting(&build, tile.0, Recipe::Tools) | |
| 1309 | + .or_else(|| find_workbench(&build, tile.0)) | |
| 1310 | + }) | |
| 1200 | 1311 | .flatten() |
| 1201 | 1312 | }; |
| 1202 | 1313 | let designated = || { |
| @@ -1208,7 +1319,7 @@ fn choose_jobs( | ||
| 1208 | 1319 | }) |
| 1209 | 1320 | .flatten() |
| 1210 | 1321 | }; |
| 1211 | - let candidates: [(WorkType, &dyn Fn() -> Option<Task>); 15] = [ | |
| 1322 | + let candidates: [(WorkType, &dyn Fn() -> Option<Task>); 16] = [ | |
| 1212 | 1323 | (WorkType::Haul, &|| { |
| 1213 | 1324 | find_expansion(&map, &zones, &reservations, &snapshot, &blueprints) |
| 1214 | 1325 | }), |
| @@ -1239,6 +1350,7 @@ fn choose_jobs( | ||
| 1239 | 1350 | .flatten() |
| 1240 | 1351 | }), |
| 1241 | 1352 | (WorkType::Build, &|| find_bed_site(&build, adults, tile.0)), |
| 1353 | + (WorkType::Craft, &toolmaking), | |
| 1242 | 1354 | (WorkType::Build, &|| find_blueprint_work(&build, tile.0)), |
| 1243 | 1355 | (WorkType::Gather, &|| { |
| 1244 | 1356 | ItemKind::ALL |
| @@ -1281,7 +1393,7 @@ fn choose_jobs( | ||
| 1281 | 1393 | Task::Hunt { kind, .. } => { |
| 1282 | 1394 | *pending.entry(ItemKind::Meat).or_insert(0) += kind.meat() |
| 1283 | 1395 | } |
| 1284 | - Task::Cook { .. } => *pending.entry(ItemKind::Meal).or_insert(0) += 1, | |
| 1396 | + Task::Craft { recipe, .. } => *pending.entry(recipe.output()).or_insert(0) += 1, | |
| 1285 | 1397 | _ => {} |
| 1286 | 1398 | } |
| 1287 | 1399 | if let Task::Build { |
| @@ -1356,6 +1468,7 @@ fn run_jobs( | ||
| 1356 | 1468 | &Rest, |
| 1357 | 1469 | &mut Hunger, |
| 1358 | 1470 | Has<Sick>, |
| 1471 | + Option<&mut Tool>, | |
| 1359 | 1472 | ), |
| 1360 | 1473 | With<Colonist>, |
| 1361 | 1474 | >, |
| @@ -1372,8 +1485,18 @@ fn run_jobs( | ||
| 1372 | 1485 | ) |
| 1373 | 1486 | .flatten() |
| 1374 | 1487 | .collect(); |
| 1375 | - for (entity, tile, mut path, mut current, mut idle, mut carrying, rest, mut hunger, sick) in | |
| 1376 | - &mut colonists | |
| 1488 | + for ( | |
| 1489 | + entity, | |
| 1490 | + tile, | |
| 1491 | + mut path, | |
| 1492 | + mut current, | |
| 1493 | + mut idle, | |
| 1494 | + mut carrying, | |
| 1495 | + rest, | |
| 1496 | + mut hunger, | |
| 1497 | + sick, | |
| 1498 | + mut tool, | |
| 1499 | + ) in &mut colonists | |
| 1377 | 1500 | { |
| 1378 | 1501 | let Some(mut job) = current.0 else { |
| 1379 | 1502 | continue; |
| @@ -1382,20 +1505,52 @@ fn run_jobs( | ||
| 1382 | 1505 | rate if sick => rate.div_ceil(2), |
| 1383 | 1506 | rate => rate, |
| 1384 | 1507 | }; |
| 1508 | + let rate = tool_rate(rate, tool.is_some()); | |
| 1385 | 1509 | if !path.0.is_empty() { |
| 1386 | 1510 | continue; |
| 1387 | 1511 | } |
| 1512 | + if let Some(tool) = tool.as_mut() | |
| 1513 | + && !matches!( | |
| 1514 | + job.task, | |
| 1515 | + Task::MoveTo(_) | Task::Sleep { .. } | Task::Eat { .. } | Task::Equip { .. } | |
| 1516 | + ) | |
| 1517 | + { | |
| 1518 | + tool.wear += 1; | |
| 1519 | + if tool.wear >= TOOL_LIFE_TICKS { | |
| 1520 | + commands.entity(entity).remove::<Tool>(); | |
| 1521 | + } | |
| 1522 | + } | |
| 1388 | 1523 | let step = match &mut job.task { |
| 1389 | 1524 | Task::MoveTo(_) => Step::Done, |
| 1390 | - Task::Cook { | |
| 1525 | + Task::Equip { pickup } => { | |
| 1526 | + let from = *pickup; | |
| 1527 | + if reservations.holder(Target::Pickup(from)) != Some(entity) { | |
| 1528 | + Step::Done | |
| 1529 | + } else if tile.0 != from { | |
| 1530 | + walk_to(&map, tile.0, from, &mut path) | |
| 1531 | + } else { | |
| 1532 | + reservations.release(Target::Pickup(from)); | |
| 1533 | + let stack = items | |
| 1534 | + .iter() | |
| 1535 | + .any(|(_, item, pos)| pos.0 == from && item.kind == ItemKind::Tools); | |
| 1536 | + if stack && take_items(&mut commands, &mut items, from, 1).is_some() { | |
| 1537 | + goals.record_use(ItemKind::Tools, 1); | |
| 1538 | + commands.entity(entity).insert(Tool::default()); | |
| 1539 | + } | |
| 1540 | + Step::Done | |
| 1541 | + } | |
| 1542 | + } | |
| 1543 | + Task::Craft { | |
| 1544 | + recipe, | |
| 1391 | 1545 | pickup, |
| 1392 | 1546 | station, |
| 1393 | 1547 | progress, |
| 1394 | 1548 | } => { |
| 1395 | - let station = *station; | |
| 1549 | + let (recipe, station) = (*recipe, *station); | |
| 1550 | + let input = recipe.input(); | |
| 1396 | 1551 | let fire = structures |
| 1397 | 1552 | .iter() |
| 1398 | - .any(|(_, pos, s, _)| pos.0 == station && s.kind == StructureKind::Campfire); | |
| 1553 | + .any(|(_, pos, s, _)| pos.0 == station && s.kind == recipe.station()); | |
| 1399 | 1554 | let ours = reservations.holder(Target::Station(station)) == Some(entity); |
| 1400 | 1555 | if !fire || !ours { |
| 1401 | 1556 | Step::Done |
| @@ -1405,12 +1560,12 @@ fn run_jobs( | ||
| 1405 | 1560 | } else if tile.0 != from { |
| 1406 | 1561 | walk_to(&map, tile.0, from, &mut path) |
| 1407 | 1562 | } else { |
| 1408 | - let load = take_items(&mut commands, &mut items, from, COOK_INPUT) | |
| 1409 | - .filter(|load| load.kind.is_raw_food() && load.count > 0); | |
| 1563 | + let load = take_items(&mut commands, &mut items, from, input) | |
| 1564 | + .filter(|load| recipe.accepts(load.kind) && load.count > 0); | |
| 1410 | 1565 | carrying.0 = load; |
| 1411 | 1566 | reservations.release(Target::Pickup(from)); |
| 1412 | 1567 | match load { |
| 1413 | - Some(load) if load.count >= COOK_INPUT => { | |
| 1568 | + Some(load) if load.count >= input => { | |
| 1414 | 1569 | *pickup = None; |
| 1415 | 1570 | Step::Continue |
| 1416 | 1571 | } |
| @@ -1419,7 +1574,7 @@ fn run_jobs( | ||
| 1419 | 1574 | } |
| 1420 | 1575 | } else if !carrying |
| 1421 | 1576 | .0 |
| 1422 | - .is_some_and(|load| load.kind.is_raw_food() && load.count >= COOK_INPUT) | |
| 1577 | + .is_some_and(|load| recipe.accepts(load.kind) && load.count >= input) | |
| 1423 | 1578 | { |
| 1424 | 1579 | Step::Done |
| 1425 | 1580 | } else if !is_adjacent(tile.0.as_ivec2(), station.as_ivec2()) { |
| @@ -1432,15 +1587,15 @@ fn run_jobs( | ||
| 1432 | 1587 | } |
| 1433 | 1588 | } else { |
| 1434 | 1589 | *progress += rate; |
| 1435 | - if *progress < COOK_WORK { | |
| 1590 | + if *progress < recipe.work() { | |
| 1436 | 1591 | Step::Continue |
| 1437 | 1592 | } else { |
| 1438 | 1593 | if let Some(load) = carrying.0 { |
| 1439 | - goals.record_use(load.kind, COOK_INPUT); | |
| 1594 | + goals.record_use(load.kind, input); | |
| 1440 | 1595 | } |
| 1441 | 1596 | carrying.0 = carrying.0.and_then(|load| { |
| 1442 | - (load.count > COOK_INPUT).then_some(Item { | |
| 1443 | - count: load.count - COOK_INPUT, | |
| 1597 | + (load.count > input).then_some(Item { | |
| 1598 | + count: load.count - input, | |
| 1444 | 1599 | ..load |
| 1445 | 1600 | }) |
| 1446 | 1601 | }); |
| @@ -1449,7 +1604,7 @@ fn run_jobs( | ||
| 1449 | 1604 | &mut items, |
| 1450 | 1605 | &item_assets, |
| 1451 | 1606 | tile.0, |
| 1452 | - Item::new(ItemKind::Meal, 1), | |
| 1607 | + Item::new(recipe.output(), 1), | |
| 1453 | 1608 | ); |
| 1454 | 1609 | Step::Done |
| 1455 | 1610 | } |
| @@ -1684,7 +1839,10 @@ fn run_jobs( | ||
| 1684 | 1839 | let ours = reservations.holder(Target::Site(site)) == Some(entity); |
| 1685 | 1840 | let planned = match kind { |
| 1686 | 1841 | BuildKind::Structure( |
| 1687 | - StructureKind::Campfire | StructureKind::Torch | StructureKind::Bed, | |
| 1842 | + StructureKind::Campfire | |
| 1843 | + | StructureKind::Torch | |
| 1844 | + | StructureKind::Bed | |
| 1845 | + | StructureKind::Workbench, | |
| 1688 | 1846 | ) |
| 1689 | 1847 | | BuildKind::Refuel(_) => true, |
| 1690 | 1848 | _ => blueprints.get(site) == Some(kind), |
src/jobs/tests.rs +45 -0
| @@ -1421,3 +1421,48 @@ fn cold_colonists_warm_up_by_a_fire() { | ||
| 1421 | 1421 | }; |
| 1422 | 1422 | assert!(spot.as_vec2().distance(Vec2::splat(20.0)) <= 3.0); |
| 1423 | 1423 | } |
| 1424 | + | |
| 1425 | +// Tools are made from iron at a workbench when the colony wants more, and colonists without | |
| 1426 | +// tools pick one up; working with tools is half again as fast. | |
| 1427 | +#[test] | |
| 1428 | +fn colonists_make_tools_at_a_workbench_and_equip_them() { | |
| 1429 | + let mut app = test_app(WorldMap::filled(UVec2::splat(30), Terrain::Grass)); | |
| 1430 | + set_goals(&mut app, &[(ItemKind::Tools, 1)]); | |
| 1431 | + app.world_mut().spawn(( | |
| 1432 | + Structure { | |
| 1433 | + kind: StructureKind::Workbench, | |
| 1434 | + }, | |
| 1435 | + TilePos(UVec2::new(15, 15)), | |
| 1436 | + )); | |
| 1437 | + app.world_mut().spawn(( | |
| 1438 | + Item::new(ItemKind::Iron, TOOL_IRON), | |
| 1439 | + TilePos(UVec2::new(5, 5)), | |
| 1440 | + )); | |
| 1441 | + let colonist = spawn_colonist(&mut app, UVec2::new(2, 2)); | |
| 1442 | + run_ticks(&mut app, 3000); | |
| 1443 | + assert_eq!(items_of(&mut app, ItemKind::Iron), 0); | |
| 1444 | + assert!(app.world().get::<Tool>(colonist).is_some()); | |
| 1445 | + assert_eq!(tool_rate(2, true), 3); | |
| 1446 | + assert_eq!(tool_rate(1, true), 2); | |
| 1447 | +} | |
| 1448 | + | |
| 1449 | +#[test] | |
| 1450 | +fn a_workbench_is_built_when_tools_are_wanted() { | |
| 1451 | + let mut app = test_app(WorldMap::filled(UVec2::splat(30), Terrain::Grass)); | |
| 1452 | + set_goals(&mut app, &[(ItemKind::Tools, 1)]); | |
| 1453 | + put_wood(&mut app, UVec2::new(5, 5), 50); | |
| 1454 | + put_beds(&mut app, 1); | |
| 1455 | + app.world_mut().spawn(( | |
| 1456 | + Item::new(ItemKind::Iron, TOOL_IRON), | |
| 1457 | + TilePos(UVec2::new(6, 5)), | |
| 1458 | + )); | |
| 1459 | + spawn_colonist(&mut app, UVec2::new(2, 2)); | |
| 1460 | + run_ticks(&mut app, 4000); | |
| 1461 | + let world = app.world_mut(); | |
| 1462 | + let benches = world | |
| 1463 | + .query::<&Structure>() | |
| 1464 | + .iter(world) | |
| 1465 | + .filter(|s| s.kind == StructureKind::Workbench) | |
| 1466 | + .count(); | |
| 1467 | + assert_eq!(benches, 1); | |
| 1468 | +} | |
src/mood.rs +16 -3
| @@ -11,6 +11,7 @@ use crate::{ | ||
| 11 | 11 | population::ColonyLog, |
| 12 | 12 | rng::SimRng, |
| 13 | 13 | sim::{SimSystems, SimTick, TICKS_PER_HOUR}, |
| 14 | + structures::{Structure, StructureKind}, | |
| 14 | 15 | world::{TilePos, WorldMap}, |
| 15 | 16 | worldgen::WorldGenerated, |
| 16 | 17 | }; |
| @@ -107,6 +108,7 @@ pub struct MoodFactors { | ||
| 107 | 108 | pub home: bool, |
| 108 | 109 | pub grieving: bool, |
| 109 | 110 | pub cold: bool, |
| 111 | + pub beauty: bool, | |
| 110 | 112 | } |
| 111 | 113 | |
| 112 | 114 | pub fn mood_target(f: MoodFactors) -> f32 { |
| @@ -133,6 +135,9 @@ pub fn mood_target(f: MoodFactors) -> f32 { | ||
| 133 | 135 | if f.cold { |
| 134 | 136 | mood -= 0.15; |
| 135 | 137 | } |
| 138 | + if f.beauty { | |
| 139 | + mood += 0.1; | |
| 140 | + } | |
| 136 | 141 | mood.clamp(0.0, 1.0) |
| 137 | 142 | } |
| 138 | 143 | |
| @@ -184,6 +189,7 @@ fn update_mood( | ||
| 184 | 189 | homes: Res<Homes>, |
| 185 | 190 | map: Res<WorldMap>, |
| 186 | 191 | losses: Res<Losses>, |
| 192 | + structures: Query<(&TilePos, &Structure), Without<Colonist>>, | |
| 187 | 193 | mut colonists: Query<( |
| 188 | 194 | Entity, |
| 189 | 195 | &mut Mood, |
| @@ -200,14 +206,19 @@ fn update_mood( | ||
| 200 | 206 | return; |
| 201 | 207 | } |
| 202 | 208 | let grieving = losses.0.iter().any(|&t| tick.0 < t + GRIEF_TICKS); |
| 209 | + let statues: Vec<UVec2> = structures | |
| 210 | + .iter() | |
| 211 | + .filter(|(_, s)| s.kind == StructureKind::Statue) | |
| 212 | + .map(|(pos, _)| pos.0) | |
| 213 | + .collect(); | |
| 203 | 214 | for (entity, mut mood, hunger, rest, sick, meal, partner, family, warmth) in &mut colonists { |
| 204 | 215 | let sick_now = sick.is_some_and(|s| tick.0 < s.until); |
| 205 | 216 | if sick.is_some() && !sick_now { |
| 206 | 217 | commands.entity(entity).remove::<Sick>(); |
| 207 | 218 | } |
| 208 | - let home = homes | |
| 209 | - .of(family.copied()) | |
| 210 | - .is_some_and(|h| h.interior().all(|t| map.is_roofed(t.as_ivec2()))); | |
| 219 | + let own_home = homes.of(family.copied()); | |
| 220 | + let home = own_home.is_some_and(|h| h.interior().all(|t| map.is_roofed(t.as_ivec2()))); | |
| 221 | + let beauty = own_home.is_some_and(|h| statues.iter().any(|&t| h.contains(t))); | |
| 211 | 222 | let target = mood_target(MoodFactors { |
| 212 | 223 | fed: hunger.0, |
| 213 | 224 | rest: rest.0, |
| @@ -219,6 +230,7 @@ fn update_mood( | ||
| 219 | 230 | home, |
| 220 | 231 | grieving, |
| 221 | 232 | cold: warmth.cold(), |
| 233 | + beauty, | |
| 222 | 234 | }); |
| 223 | 235 | mood.0 += (target - mood.0) / MOOD_HOURS; |
| 224 | 236 | } |
| @@ -327,6 +339,7 @@ mod tests { | ||
| 327 | 339 | let settled = mood_target(MoodFactors { |
| 328 | 340 | partner: true, |
| 329 | 341 | home: true, |
| 342 | + beauty: true, | |
| 330 | 343 | last_meal_cooked: Some(true), |
| 331 | 344 | ..content() |
| 332 | 345 | }); |
src/selection.rs +25 -8
| @@ -12,7 +12,7 @@ use crate::{ | ||
| 12 | 12 | family::{Child, Partner}, |
| 13 | 13 | food::Hunger, |
| 14 | 14 | items::{Carrying, Item, stock}, |
| 15 | - jobs::{BUTCHER_WORK, COOK_WORK, CurrentJob, EXPAND_WORK, Job, Order, Task, WorkKind}, | |
| 15 | + jobs::{BUTCHER_WORK, CurrentJob, EXPAND_WORK, Job, Order, Recipe, Task, Tool, WorkKind}, | |
| 16 | 16 | light::daylight, |
| 17 | 17 | mood::{Age, Mood, Sick}, |
| 18 | 18 | needs::Rest, |
| @@ -280,6 +280,7 @@ fn update_tile_info( | ||
| 280 | 280 | Option<&Age>, |
| 281 | 281 | Has<Sick>, |
| 282 | 282 | Option<&Warmth>, |
| 283 | + Has<Tool>, | |
| 283 | 284 | )>, |
| 284 | 285 | mut text: Single<&mut Text, With<TileInfoText>>, |
| 285 | 286 | ) { |
| @@ -300,7 +301,7 @@ fn update_tile_info( | ||
| 300 | 301 | fed: hunger.0, |
| 301 | 302 | }) + &kin |
| 302 | 303 | .get(entity) |
| 303 | - .map(|(partner, child, mood, age, sick, warmth)| { | |
| 304 | + .map(|(partner, child, mood, age, sick, warmth, tool)| { | |
| 304 | 305 | let partner = partner |
| 305 | 306 | .and_then(|p| colonists.get(p.0).ok()) |
| 306 | 307 | .map(|p| format!(", partner {}", p.0.name)); |
| @@ -313,10 +314,11 @@ fn update_tile_info( | ||
| 313 | 314 | .map(|a| format!(", age {}", a.years(*tick))) |
| 314 | 315 | .unwrap_or_default(); |
| 315 | 316 | let sick = if sick { ", sick" } else { "" }; |
| 317 | + let tool = if tool { ", has tools" } else { "" }; | |
| 316 | 318 | let warmth = warmth |
| 317 | 319 | .map(|w| { |
| 318 | 320 | let cold = if w.cold() { " (cold)" } else { "" }; |
| 319 | - format!(", {:.0}°C{cold}", w.0) | |
| 321 | + format!(", {:.0}C{cold}", w.0) | |
| 320 | 322 | }) |
| 321 | 323 | .unwrap_or_default(); |
| 322 | 324 | partner.unwrap_or_default() |
| @@ -324,6 +326,7 @@ fn update_tile_info( | ||
| 324 | 326 | + &mood |
| 325 | 327 | + &age |
| 326 | 328 | + sick |
| 329 | + + tool | |
| 327 | 330 | + &warmth |
| 328 | 331 | }) |
| 329 | 332 | .unwrap_or_default() |
| @@ -331,7 +334,7 @@ fn update_tile_info( | ||
| 331 | 334 | .unwrap_or_else(|_| "-".into()), |
| 332 | 335 | }; |
| 333 | 336 | let clock = format!( |
| 334 | - "{} ({}), {} day {}, {:.0}°C", | |
| 337 | + "{} ({}), {} day {}, {:.0}C", | |
| 335 | 338 | format_clock(*tick), |
| 336 | 339 | time_of_day(hour_of_day(*tick)), |
| 337 | 340 | season(*tick).name(), |
| @@ -502,7 +505,9 @@ pub fn progress_fraction(task: Task) -> Option<f32> { | ||
| 502 | 505 | Task::ExpandStockpile { progress, .. } => Some(progress as f32 / EXPAND_WORK as f32), |
| 503 | 506 | Task::Build { kind, progress, .. } => Some(progress as f32 / kind.work() as f32), |
| 504 | 507 | Task::Hunt { kind, progress, .. } => Some(progress as f32 / kind.hunt_work() as f32), |
| 505 | - Task::Cook { progress, .. } => Some(progress as f32 / COOK_WORK as f32), | |
| 508 | + Task::Craft { | |
| 509 | + progress, recipe, .. | |
| 510 | + } => Some(progress as f32 / recipe.work() as f32), | |
| 506 | 511 | Task::Butcher { progress, .. } => Some(progress as f32 / BUTCHER_WORK as f32), |
| 507 | 512 | Task::Tame { kind, progress, .. } => Some(progress as f32 / kind.tame_work() as f32), |
| 508 | 513 | _ => None, |
| @@ -597,13 +602,25 @@ pub fn describe_colonist(view: &ColonistView) -> String { | ||
| 597 | 602 | } |
| 598 | 603 | (Some(Task::Hunt { kind, .. }), _) => format!("stalking a {}", kind.name()), |
| 599 | 604 | ( |
| 600 | - Some(Task::Cook { | |
| 605 | + Some(Task::Craft { | |
| 601 | 606 | pickup: Some(_), .. |
| 602 | 607 | }), |
| 603 | 608 | _, |
| 604 | 609 | ) => "fetching ingredients".to_string(), |
| 605 | - (Some(Task::Cook { .. }), n) if n > 0 => "carrying ingredients to the fire".to_string(), | |
| 606 | - (Some(task @ Task::Cook { .. }), _) => format!("cooking a meal, {}%", percent(task)), | |
| 610 | + (Some(Task::Craft { recipe, .. }), n) if n > 0 => { | |
| 611 | + format!("carrying ingredients to the {}", recipe.station().name()) | |
| 612 | + } | |
| 613 | + ( | |
| 614 | + Some( | |
| 615 | + task @ Task::Craft { | |
| 616 | + recipe: Recipe::Meal, | |
| 617 | + .. | |
| 618 | + }, | |
| 619 | + ), | |
| 620 | + _, | |
| 621 | + ) => format!("cooking a meal, {}%", percent(task)), | |
| 622 | + (Some(task @ Task::Craft { .. }), _) => format!("making tools, {}%", percent(task)), | |
| 623 | + (Some(Task::Equip { .. }), _) => "fetching tools".to_string(), | |
| 607 | 624 | (Some(Task::Butcher { .. }), n) if n > 0 => "walking to a carcass".to_string(), |
| 608 | 625 | (Some(Task::Tame { kind, .. }), n) if n > 0 => format!("approaching a {}", kind.name()), |
| 609 | 626 | (Some(task @ Task::Tame { kind, .. }), _) => { |
src/structures.rs +50 -3
| @@ -27,6 +27,8 @@ pub enum StructureKind { | ||
| 27 | 27 | Bed, |
| 28 | 28 | Wall, |
| 29 | 29 | Door, |
| 30 | + Workbench, | |
| 31 | + Statue, | |
| 30 | 32 | } |
| 31 | 33 | |
| 32 | 34 | impl StructureKind { |
| @@ -37,6 +39,8 @@ impl StructureKind { | ||
| 37 | 39 | StructureKind::Bed => (ItemKind::Wood, 15), |
| 38 | 40 | StructureKind::Wall => (ItemKind::Stone, 2), |
| 39 | 41 | StructureKind::Door => (ItemKind::Wood, 5), |
| 42 | + StructureKind::Workbench => (ItemKind::Wood, 20), | |
| 43 | + StructureKind::Statue => (ItemKind::Gold, 5), | |
| 40 | 44 | } |
| 41 | 45 | } |
| 42 | 46 | |
| @@ -44,7 +48,7 @@ impl StructureKind { | ||
| 44 | 48 | let hours = match self { |
| 45 | 49 | StructureKind::Campfire => 12, |
| 46 | 50 | StructureKind::Torch => 8, |
| 47 | - StructureKind::Bed | StructureKind::Wall | StructureKind::Door => return None, | |
| 51 | + _ => return None, | |
| 48 | 52 | }; |
| 49 | 53 | Some(hours * TICKS_PER_HOUR as u32) |
| 50 | 54 | } |
| @@ -53,7 +57,7 @@ impl StructureKind { | ||
| 53 | 57 | match self { |
| 54 | 58 | StructureKind::Campfire => Some((7.0, Vec3::new(1.1, 0.8, 0.5))), |
| 55 | 59 | StructureKind::Torch => Some((4.0, Vec3::new(1.0, 0.8, 0.55))), |
| 56 | - StructureKind::Bed | StructureKind::Wall | StructureKind::Door => None, | |
| 60 | + _ => None, | |
| 57 | 61 | } |
| 58 | 62 | } |
| 59 | 63 | |
| @@ -61,7 +65,7 @@ impl StructureKind { | ||
| 61 | 65 | match self { |
| 62 | 66 | StructureKind::Campfire => 20.0, |
| 63 | 67 | StructureKind::Torch => 4.0, |
| 64 | - StructureKind::Bed | StructureKind::Wall | StructureKind::Door => 0.0, | |
| 68 | + _ => 0.0, | |
| 65 | 69 | } |
| 66 | 70 | } |
| 67 | 71 | |
| @@ -76,6 +80,8 @@ impl StructureKind { | ||
| 76 | 80 | StructureKind::Bed => "bed", |
| 77 | 81 | StructureKind::Wall => "wall", |
| 78 | 82 | StructureKind::Door => "door", |
| 83 | + StructureKind::Workbench => "workbench", | |
| 84 | + StructureKind::Statue => "statue", | |
| 79 | 85 | } |
| 80 | 86 | } |
| 81 | 87 | } |
| @@ -120,6 +126,11 @@ pub struct StructureAssets { | ||
| 120 | 126 | wall_top: Part, |
| 121 | 127 | door: Part, |
| 122 | 128 | door_knob: Part, |
| 129 | + bench: Part, | |
| 130 | + bench_leg: Part, | |
| 131 | + anvil: Part, | |
| 132 | + plinth: Part, | |
| 133 | + gold: Part, | |
| 123 | 134 | } |
| 124 | 135 | |
| 125 | 136 | const CAMPFIRE_STONES: usize = 7; |
| @@ -188,6 +199,31 @@ fn create_structure_assets( | ||
| 188 | 199 | Rectangle::new(TILE_SIZE * 0.12, TILE_SIZE * 0.12).into(), |
| 189 | 200 | Color::srgb_u8(60, 44, 30), |
| 190 | 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 | + ), | |
| 191 | 227 | }); |
| 192 | 228 | } |
| 193 | 229 | |
| @@ -242,6 +278,17 @@ pub fn spawn_structure( | ||
| 242 | 278 | c.spawn(part_bundle(&assets.door, at(0.0, 0.0, 0.0))); |
| 243 | 279 | c.spawn(part_bundle(&assets.door_knob, at(0.25, 0.0, 0.01))); |
| 244 | 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 | + }), | |
| 245 | 292 | StructureKind::Torch => entity.with_children(|c| { |
| 246 | 293 | c.spawn(part_bundle(&assets.stick, at(0.0, -0.15, 0.0))); |
| 247 | 294 | let small = Vec3::splat(0.6); |