an http based roguelike rpg about jelqing
| 1 | use super::{ |
| 2 | Grip, Lube, Outcome, Run, |
| 3 | balance::{ |
| 4 | ARRIVAL_BONUS, BAND_BONUS, CLEAN_HIT, DANGER_EQ, EQ_BAND, GREED_HOT, GREED_WARM, HOT_EQ, |
| 5 | NEAR_MISS, QUEUED_ETA, RING_RUPTURE_LINE, RUPTURE_LINE, UNDERHAND_STRETCH, UNDERHAND_WET, |
| 6 | }, |
| 7 | body::Pending, |
| 8 | measure::round3, |
| 9 | techniques::{Profile, Training}, |
| 10 | }; |
| 11 | use crate::{headers::SERVER_TIMING, problem::Problem}; |
| 12 | use axum::http::{StatusCode, header}; |
| 13 | use serde_json::json; |
| 14 | |
| 15 | impl Run { |
| 16 | pub(super) fn roll( |
| 17 | &mut self, |
| 18 | training: Training, |
| 19 | intensity: i32, |
| 20 | lube: Option<Lube>, |
| 21 | ) -> Result<Outcome, Problem> { |
| 22 | let profile = training.profile(); |
| 23 | let natural: i32 = self.oracle.span_int( |
| 24 | self.decision( |
| 25 | "natural", |
| 26 | "What does the twenty-sided die show for this technique?", |
| 27 | ), |
| 28 | 1, |
| 29 | 20, |
| 30 | ); |
| 31 | let modifier = self.modifier(training, lube); |
| 32 | let dc = profile.dc + intensity / 3; |
| 33 | let total = natural + modifier; |
| 34 | let timing = format!("roll;desc=\"d20{modifier:+} vs {dc}\";dur={natural}"); |
| 35 | let timed = |p: Problem| p.header(SERVER_TIMING, &timing); |
| 36 | |
| 37 | let (length, girth) = self.scaled_gains(&profile, intensity); |
| 38 | let rupture_line = if self.items.kit.ring { |
| 39 | RING_RUPTURE_LINE |
| 40 | } else { |
| 41 | RUPTURE_LINE |
| 42 | }; |
| 43 | |
| 44 | if natural == 1 { |
| 45 | return Err(timed(self.critical_failure(training, &profile))); |
| 46 | } |
| 47 | self.here.warm_arrival = false; |
| 48 | if natural == 2 && self.hotter_than(DANGER_EQ) { |
| 49 | return Err(timed(self.rupture())); |
| 50 | } |
| 51 | if self.hotter_than(rupture_line) && total < dc { |
| 52 | return Err(timed(self.rupture())); |
| 53 | } |
| 54 | if natural == 20 { |
| 55 | self.gain(length * 2.0, girth * 2.0, profile.foreskin_growth); |
| 56 | return Ok(Outcome::json( |
| 57 | StatusCode::PERMANENT_REDIRECT, |
| 58 | json!({ |
| 59 | "detail": "Your length has moved permanently.", |
| 60 | "length": round3(length * 2.0), |
| 61 | "girth": round3(girth * 2.0), |
| 62 | }), |
| 63 | ) |
| 64 | .header(header::LOCATION, self.path("/length")) |
| 65 | .header(SERVER_TIMING, timing)); |
| 66 | } |
| 67 | if total < dc - NEAR_MISS { |
| 68 | return Err(timed(not_extended())); |
| 69 | } |
| 70 | if total < dc { |
| 71 | return Ok(Outcome::empty(StatusCode::NO_CONTENT).header(SERVER_TIMING, timing)); |
| 72 | } |
| 73 | Ok(self |
| 74 | .land(&profile, length, girth, total >= dc + CLEAN_HIT) |
| 75 | .header(SERVER_TIMING, timing)) |
| 76 | } |
| 77 | |
| 78 | fn modifier(&self, training: Training, lube: Option<Lube>) -> i32 { |
| 79 | let band = if EQ_BAND.contains(&self.body.eq) { |
| 80 | BAND_BONUS |
| 81 | } else { |
| 82 | 0 |
| 83 | }; |
| 84 | let grip = match (self.body.grip, training) { |
| 85 | (Grip::Underhand, Training::Stretch) => UNDERHAND_STRETCH, |
| 86 | (Grip::Underhand, Training::Wet) => UNDERHAND_WET, |
| 87 | _ => 0, |
| 88 | }; |
| 89 | let arrival = if self.here.warm_arrival { |
| 90 | ARRIVAL_BONUS |
| 91 | } else { |
| 92 | 0 |
| 93 | }; |
| 94 | lube.map_or(0, Lube::modifier) + band + grip + arrival |
| 95 | } |
| 96 | |
| 97 | fn land(&mut self, profile: &Profile, length: f64, girth: f64, clean: bool) -> Outcome { |
| 98 | if self.body.eq < *EQ_BAND.start() { |
| 99 | self.gain(length / 2.0, girth / 2.0, profile.foreskin_growth); |
| 100 | Outcome::json( |
| 101 | StatusCode::PARTIAL_CONTENT, |
| 102 | json!({ |
| 103 | "detail": "Partial gains were applied. EQ was below the band.", |
| 104 | "length": round3(length / 2.0), |
| 105 | "girth": round3(girth / 2.0), |
| 106 | }), |
| 107 | ) |
| 108 | } else if clean { |
| 109 | self.gain(length, girth, profile.foreskin_growth); |
| 110 | Outcome::json( |
| 111 | StatusCode::OK, |
| 112 | json!({ |
| 113 | "detail": "Gains were applied.", |
| 114 | "length": round3(length), |
| 115 | "girth": round3(girth), |
| 116 | }), |
| 117 | ) |
| 118 | } else { |
| 119 | let eta = self.oracle.span_int( |
| 120 | self.decision("eta", "In how many turns will the queued gains arrive?"), |
| 121 | *QUEUED_ETA.start(), |
| 122 | *QUEUED_ETA.end(), |
| 123 | ); |
| 124 | self.body.pending.push(Pending { |
| 125 | due: self.turn + eta.unsigned_abs(), |
| 126 | length, |
| 127 | girth, |
| 128 | foreskin: profile.foreskin_growth, |
| 129 | }); |
| 130 | Outcome::json( |
| 131 | StatusCode::ACCEPTED, |
| 132 | json!({ |
| 133 | "detail": "Gains have been queued. They may arrive.", |
| 134 | "eta_turns": eta, |
| 135 | }), |
| 136 | ) |
| 137 | } |
| 138 | } |
| 139 | |
| 140 | pub(super) fn scaled_gains(&self, profile: &Profile, intensity: i32) -> (f64, f64) { |
| 141 | let scale = f64::from(intensity); |
| 142 | let (mut length, mut girth) = (profile.length * scale, profile.girth * scale); |
| 143 | let greed = if self.body.eq > HOT_EQ { |
| 144 | GREED_HOT |
| 145 | } else if self.body.eq > DANGER_EQ { |
| 146 | GREED_WARM |
| 147 | } else { |
| 148 | 1.0 |
| 149 | }; |
| 150 | length *= greed; |
| 151 | girth *= greed; |
| 152 | if self.here.teapot { |
| 153 | length *= 0.5; |
| 154 | girth *= 1.5; |
| 155 | } |
| 156 | if self.items.kit.ring { |
| 157 | girth *= 2.0; |
| 158 | } |
| 159 | if self.body.conditions.friction_burn { |
| 160 | length *= 0.5; |
| 161 | girth *= 0.5; |
| 162 | } |
| 163 | (length, girth) |
| 164 | } |
| 165 | |
| 166 | pub(super) fn critical_failure(&mut self, training: Training, profile: &Profile) -> Problem { |
| 167 | let uses_foreskin = profile.foreskin.is_some_and(|(min, _)| min > 0.0); |
| 168 | if uses_foreskin && self.body.foreskin.is_some() { |
| 169 | self.body.foreskin = None; |
| 170 | self.standing.shame += 25; |
| 171 | return Problem::new( |
| 172 | StatusCode::GONE, |
| 173 | "foreskin-lost", |
| 174 | "The technique has removed the foreskin. It will not be returning.", |
| 175 | ) |
| 176 | .header(header::CONTENT_LOCATION, self.path("/foreskin")); |
| 177 | } |
| 178 | if self.hotter_than(DANGER_EQ) { |
| 179 | return self.rupture(); |
| 180 | } |
| 181 | if training == Training::Stretch && !self.here.teapot { |
| 182 | self.here.teapot = true; |
| 183 | self.items.kit.teapot_spout = true; |
| 184 | self.standing.shame += 5; |
| 185 | return Problem::new( |
| 186 | StatusCode::IM_A_TEAPOT, |
| 187 | "teapot", |
| 188 | "The subject is now short and stout. Length gains are halved for the rest of the floor.", |
| 189 | ); |
| 190 | } |
| 191 | if training == Training::Uli && !self.body.conditions.peyronie { |
| 192 | self.body.conditions.peyronie = true; |
| 193 | self.standing.shame += 5; |
| 194 | return Problem::new( |
| 195 | StatusCode::MISDIRECTED_REQUEST, |
| 196 | "curvature", |
| 197 | "A curvature has developed. Techniques will land slightly to the left until a professional is consulted.", |
| 198 | ); |
| 199 | } |
| 200 | not_extended() |
| 201 | } |
| 202 | |
| 203 | pub(super) fn gossip(&mut self, outcome: Outcome) -> Outcome { |
| 204 | let reportable = matches!(outcome.status().as_u16(), 200 | 202 | 206 | 308); |
| 205 | if !self.here.brother_told || !reportable { |
| 206 | return outcome; |
| 207 | } |
| 208 | self.here.brother_told = false; |
| 209 | outcome.retold( |
| 210 | StatusCode::ALREADY_REPORTED, |
| 211 | "Your brother has already told Mum.", |
| 212 | ) |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | pub(super) fn not_extended() -> Problem { |
| 217 | Problem::new( |
| 218 | StatusCode::NOT_EXTENDED, |
| 219 | "not-extended", |
| 220 | "The requested extension was not applied. The server has recorded the attempt.", |
| 221 | ) |
| 222 | } |