an http based roguelike rpg about jelqing
Blame: src/game/kitchen.rs
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 1 | use axum::http::StatusCode; |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 2 | use serde_json::json; |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 3 | |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 4 | use super::{ |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 5 | Cause, Choice, Floor, Item, Notice, Outcome, Run, Story, |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 6 | balance::{ |
f16ab17c8ff6 | atagen | 2026-10-13 11:14 UTC | 7 | CAUGHT_MELONED, COCONUT_ODDS, ENERGY_ODDS, KETTLE_ODDS, KITCHEN_ICE_ODDS, KITCHEN_TOLD, |
f16ab17c8ff6 | atagen | 2026-10-13 11:14 UTC | 8 | MELON_SHAME, RAID_NOISE, RUPTURE_LINE, WATERMELON, |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 9 | }, |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 10 | }; |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 11 | use crate::problem::Problem; |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 12 | |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 13 | #[derive(Clone, Copy)] |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 14 | enum Loot { |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 15 | CoconutOil, |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 16 | EnergyDrink, |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 17 | IcePack, |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 18 | Kettle, |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 19 | Nothing, |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 20 | } |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 21 | |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 22 | impl Loot { |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 23 | const ALL: [Self; 5] = [ |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 24 | Self::CoconutOil, |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 25 | Self::EnergyDrink, |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 26 | Self::IcePack, |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 27 | Self::Kettle, |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 28 | Self::Nothing, |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 29 | ]; |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 30 | |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 31 | /// Out of 120, so one draw keeps the marginals at the documented 1 in 3, 1 in 5, 1 in 8 and 1 in 6. |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 32 | fn weight(self) -> u32 { |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 33 | match self { |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 34 | Self::CoconutOil => 120 / COCONUT_ODDS, |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 35 | Self::EnergyDrink => 120 / ENERGY_ODDS, |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 36 | Self::IcePack => 120 / KITCHEN_ICE_ODDS, |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 37 | Self::Kettle => 120 / KETTLE_ODDS, |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 38 | Self::Nothing => 120 - Self::ALL[..4].iter().map(|l| l.weight()).sum::<u32>(), |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 39 | } |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 40 | } |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 41 | |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 42 | fn sight(self) -> &'static str { |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 43 | match self { |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 44 | Self::CoconutOil => "A jar of coconut oil, half of it already gone.", |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 45 | Self::EnergyDrink => "An energy drink, cold and faintly alarming.", |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 46 | Self::IcePack => "An ice pack, buried under the peas.", |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 47 | Self::Kettle => "The kettle has just boiled.", |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 48 | Self::Nothing => "Nothing. The fridge is ordinary.", |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 49 | } |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 50 | } |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 51 | } |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 52 | |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 53 | impl Run { |
c4d70621d377 | atagen | 2026-10-12 11:56 UTC | 54 | /// The household keeps count, so every raid makes noise and the noise makes the next telling likelier. |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 55 | pub fn raid_kitchen(&mut self) -> Result<Outcome, Problem> { |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 56 | if !self.floor.has_kitchen() { |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 57 | return Err(Problem::new( |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 58 | StatusCode::NOT_FOUND, |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 59 | "no-kitchen", |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 60 | "There is no kitchen on this floor.", |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 61 | )); |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 62 | } |
f16ab17c8ff6 | atagen | 2026-10-13 11:14 UTC | 63 | if self.here.kitchen.closed { |
f16ab17c8ff6 | atagen | 2026-10-13 11:14 UTC | 64 | return Err(self.off_limits()); |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 65 | } |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 66 | if self.online.story == Story::Pending { |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 67 | return Ok(self.branch_offer()); |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 68 | } |
3ac624339c98 | atagen | 2026-10-13 12:21 UTC | 69 | self.begin_turn()?; |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 70 | if self.chance( |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 71 | self.decision("rupture", "Does this raid of the fridge rupture something?"), |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 72 | self.rupture_odds(), |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 73 | ) { |
b85a0d4536c3 | atagen | 2026-10-11 08:50 UTC | 74 | return Err(self.rupture()); |
b85a0d4536c3 | atagen | 2026-10-11 08:50 UTC | 75 | } |
c4d70621d377 | atagen | 2026-10-12 11:56 UTC | 76 | self.make_noise(RAID_NOISE); |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 77 | let loot = Loot::ALL[self.oracle.pick( |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 78 | self.decision("loot", "What does the fridge give up on this raid?"), |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 79 | &Loot::ALL.map(Loot::weight), |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 80 | &Loot::ALL.map(Loot::sight), |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 81 | )]; |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 82 | let melon = self.body.eq > RUPTURE_LINE |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 83 | && self.oracle.ratio( |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 84 | self.decision( |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 85 | "watermelon", |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 86 | "Is there a watermelon in the fridge, exactly the right shape?", |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 87 | ), |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 88 | WATERMELON.0, |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 89 | WATERMELON.1, |
c4d70621d377 | atagen | 2026-10-12 11:56 UTC | 90 | ); |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 91 | self.settle(); |
c4d70621d377 | atagen | 2026-10-12 11:56 UTC | 92 | if self.oracle.ratio( |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 93 | self.decision( |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 94 | "kitchen-told", |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 95 | "Has the household had enough of the raiding?", |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 96 | ), |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 97 | u32::from(self.standing.suspicion) + 1, |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 98 | KITCHEN_TOLD, |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 99 | ) { |
f16ab17c8ff6 | atagen | 2026-10-13 11:14 UTC | 100 | self.here.kitchen.closed = true; |
f16ab17c8ff6 | atagen | 2026-10-13 11:14 UTC | 101 | self.end_kitchen_visit(); |
aef6f6498ea2 | atagen | 2026-10-13 11:23 UTC | 102 | self.notices.push(if self.floor == Floor::Nans { |
aef6f6498ea2 | atagen | 2026-10-13 11:23 UTC | 103 | Notice::StopRaidingTheFridge |
aef6f6498ea2 | atagen | 2026-10-13 11:23 UTC | 104 | } else { |
aef6f6498ea2 | atagen | 2026-10-13 11:23 UTC | 105 | Notice::StopRaidingTheKitchen |
aef6f6498ea2 | atagen | 2026-10-13 11:23 UTC | 106 | }); |
f16ab17c8ff6 | atagen | 2026-10-13 11:14 UTC | 107 | return Err(self.off_limits()); |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 108 | } |
c4d70621d377 | atagen | 2026-10-12 11:56 UTC | 109 | if melon { |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 110 | self.here.kitchen.melon_offered = true; |
c4d70621d377 | atagen | 2026-10-12 11:56 UTC | 111 | return Ok(self.melon_offer()); |
c4d70621d377 | atagen | 2026-10-12 11:56 UTC | 112 | } |
c4d70621d377 | atagen | 2026-10-12 11:56 UTC | 113 | Ok(self.unpack(loot)) |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 114 | } |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 115 | |
f16ab17c8ff6 | atagen | 2026-10-13 11:14 UTC | 116 | fn off_limits(&self) -> Problem { |
aef6f6498ea2 | atagen | 2026-10-13 11:23 UTC | 117 | Problem::new( |
aef6f6498ea2 | atagen | 2026-10-13 11:23 UTC | 118 | StatusCode::GONE, |
aef6f6498ea2 | atagen | 2026-10-13 11:23 UTC | 119 | "kitchen-off-limits", |
aef6f6498ea2 | atagen | 2026-10-13 11:23 UTC | 120 | "The kitchen is off limits.", |
aef6f6498ea2 | atagen | 2026-10-13 11:23 UTC | 121 | ) |
f16ab17c8ff6 | atagen | 2026-10-13 11:14 UTC | 122 | } |
f16ab17c8ff6 | atagen | 2026-10-13 11:14 UTC | 123 | |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 124 | fn unpack(&mut self, loot: Loot) -> Outcome { |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 125 | let item = match loot { |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 126 | Loot::CoconutOil => Item::CoconutOil, |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 127 | Loot::EnergyDrink => Item::EnergyDrink, |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 128 | Loot::IcePack => Item::IcePack, |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 129 | Loot::Kettle => { |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 130 | self.here.kitchen.kettle_boiled = true; |
c4d70621d377 | atagen | 2026-10-12 11:56 UTC | 131 | return Outcome::json( |
c4d70621d377 | atagen | 2026-10-12 11:56 UTC | 132 | StatusCode::OK, |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 133 | json!({ "detail": format!("{} The towel can be warmed once more.", loot.sight()) }), |
c4d70621d377 | atagen | 2026-10-12 11:56 UTC | 134 | ); |
c4d70621d377 | atagen | 2026-10-12 11:56 UTC | 135 | } |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 136 | Loot::Nothing => return Outcome::empty(StatusCode::NO_CONTENT), |
c4d70621d377 | atagen | 2026-10-12 11:56 UTC | 137 | }; |
c4d70621d377 | atagen | 2026-10-12 11:56 UTC | 138 | self.items.grant(item); |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 139 | Outcome::json(StatusCode::OK, json!({ "detail": loot.sight() })) |
c4d70621d377 | atagen | 2026-10-12 11:56 UTC | 140 | } |
c4d70621d377 | atagen | 2026-10-12 11:56 UTC | 141 | |
c4d70621d377 | atagen | 2026-10-12 11:56 UTC | 142 | fn melon_offer(&self) -> Outcome { |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 143 | Outcome::choices( |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 144 | "The fridge holds a watermelon. It is exactly the right shape.", |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 145 | &[ |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 146 | Choice { |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 147 | href: self.path("/kitchen/watermelon"), |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 148 | method: "POST", |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 149 | title: "Commit to the watermelon", |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 150 | }, |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 151 | Choice { |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 152 | href: self.path(""), |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 153 | method: "GET", |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 154 | title: "Close the fridge", |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 155 | }, |
4f54028cba69 | atagen | 2026-10-14 11:45 UTC | 156 | ], |
c4d70621d377 | atagen | 2026-10-12 11:56 UTC | 157 | ) |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 158 | } |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 159 | |
c4d70621d377 | atagen | 2026-10-12 11:56 UTC | 160 | /// A bail-out that costs no turn. The more the household has already heard, the likelier Mum is in the doorway. |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 161 | pub fn commit_melon(&mut self) -> Result<Outcome, Problem> { |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 162 | if !self.here.kitchen.melon_offered { |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 163 | return Err(Problem::new( |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 164 | StatusCode::CONFLICT, |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 165 | "no-melon-on-offer", |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 166 | "There is no watermelon on offer.", |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 167 | )); |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 168 | } |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 169 | self.here.kitchen.melon_offered = false; |
14de3e1e8dc2 | atagen | 2026-10-11 06:19 UTC | 170 | self.reset_eq(0); |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 171 | self.standing.shame += MELON_SHAME; |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 172 | self.touch(); |
c4d70621d377 | atagen | 2026-10-12 11:56 UTC | 173 | if self.oracle.ratio( |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 174 | self.decision( |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 175 | "caught-meloned", |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 176 | "Is Mum standing in the doorway as you look up?", |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 177 | ), |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 178 | u32::from(self.standing.suspicion) + 2, |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 179 | CAUGHT_MELONED, |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 180 | ) { |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 181 | return Err(self.end(Cause::Meloned)); |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 182 | } |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 183 | Ok(Outcome::json( |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 184 | StatusCode::OK, |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 185 | json!({ |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 186 | "detail": "The watermelon did not survive, and neither did the mood. EQ is gone, the gains are kept, and the shame is considerable.", |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 187 | }), |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 188 | )) |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 189 | } |
0a0bfee8329e | atagen | 2026-10-11 05:11 UTC | 190 | } |