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