atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
bad9f5b8202f43735ec38acb2070d214d84302e6
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git

Blame: src/game/roll.rs

8b8e83329a1datagen2026-10-12 12:01 UTC1
use super::{
8b8e83329a1datagen2026-10-12 12:01 UTC2
    Grip, Lube, Outcome, Run,
8b8e83329a1datagen2026-10-12 12:01 UTC3
    balance::{
8b8e83329a1datagen2026-10-12 12:01 UTC4
        ARRIVAL_BONUS, BAND_BONUS, CLEAN_HIT, DANGER_EQ, EQ_BAND, GREED_HOT, GREED_WARM, HOT_EQ,
8b8e83329a1datagen2026-10-12 12:01 UTC5
        NEAR_MISS, QUEUED_ETA, RING_RUPTURE_LINE, RUPTURE_LINE, UNDERHAND_STRETCH, UNDERHAND_WET,
8b8e83329a1datagen2026-10-12 12:01 UTC6
    },
8b8e83329a1datagen2026-10-12 12:01 UTC7
    body::Pending,
8b8e83329a1datagen2026-10-12 12:01 UTC8
    measure::round3,
8b8e83329a1datagen2026-10-12 12:01 UTC9
    techniques::{Profile, Training},
8b8e83329a1datagen2026-10-12 12:01 UTC10
};
8b8e83329a1datagen2026-10-12 12:01 UTC11
use crate::{headers::SERVER_TIMING, problem::Problem};
8b8e83329a1datagen2026-10-12 12:01 UTC12
use axum::http::{StatusCode, header};
97c05b741d8fatagen2026-10-13 11:27 UTC13
use serde_json::{Value, json};
97c05b741d8fatagen2026-10-13 11:27 UTC14
97c05b741d8fatagen2026-10-13 11:27 UTC15
fn gains(detail: &str, length: f64, girth: f64, foreskin: Option<f64>) -> Value {
97c05b741d8fatagen2026-10-13 11:27 UTC16
    let mut body = json!({ "detail": detail, "length": round3(length), "girth": round3(girth) });
97c05b741d8fatagen2026-10-13 11:27 UTC17
    if let Some(foreskin) = foreskin {
97c05b741d8fatagen2026-10-13 11:27 UTC18
        body["foreskin"] = json!(round3(foreskin));
97c05b741d8fatagen2026-10-13 11:27 UTC19
    }
97c05b741d8fatagen2026-10-13 11:27 UTC20
    body
97c05b741d8fatagen2026-10-13 11:27 UTC21
}
8b8e83329a1datagen2026-10-12 12:01 UTC22
8b8e83329a1datagen2026-10-12 12:01 UTC23
impl Run {
8b8e83329a1datagen2026-10-12 12:01 UTC24
    pub(super) fn roll(
8b8e83329a1datagen2026-10-12 12:01 UTC25
        &mut self,
8b8e83329a1datagen2026-10-12 12:01 UTC26
        training: Training,
8b8e83329a1datagen2026-10-12 12:01 UTC27
        intensity: i32,
8b8e83329a1datagen2026-10-12 12:01 UTC28
        lube: Option<Lube>,
8b8e83329a1datagen2026-10-12 12:01 UTC29
    ) -> Result<Outcome, Problem> {
8b8e83329a1datagen2026-10-12 12:01 UTC30
        let profile = training.profile();
8b8e83329a1datagen2026-10-12 12:01 UTC31
        let natural: i32 = self.oracle.span_int(
8b8e83329a1datagen2026-10-12 12:01 UTC32
            self.decision(
8b8e83329a1datagen2026-10-12 12:01 UTC33
                "natural",
8b8e83329a1datagen2026-10-12 12:01 UTC34
                "What does the twenty-sided die show for this technique?",
8b8e83329a1datagen2026-10-12 12:01 UTC35
            ),
8b8e83329a1datagen2026-10-12 12:01 UTC36
            1,
8b8e83329a1datagen2026-10-12 12:01 UTC37
            20,
8b8e83329a1datagen2026-10-12 12:01 UTC38
        );
8b8e83329a1datagen2026-10-12 12:01 UTC39
        let modifier = self.modifier(training, lube);
8b8e83329a1datagen2026-10-12 12:01 UTC40
        let dc = profile.dc + intensity / 3;
8b8e83329a1datagen2026-10-12 12:01 UTC41
        let total = natural + modifier;
8b8e83329a1datagen2026-10-12 12:01 UTC42
        let timing = format!("roll;desc=\"d20{modifier:+} vs {dc}\";dur={natural}");
8b8e83329a1datagen2026-10-12 12:01 UTC43
        let timed = |p: Problem| p.header(SERVER_TIMING, &timing);
8b8e83329a1datagen2026-10-12 12:01 UTC44
8b8e83329a1datagen2026-10-12 12:01 UTC45
        let (length, girth) = self.scaled_gains(&profile, intensity);
8b8e83329a1datagen2026-10-12 12:01 UTC46
        let rupture_line = if self.items.kit.ring {
8b8e83329a1datagen2026-10-12 12:01 UTC47
            RING_RUPTURE_LINE
8b8e83329a1datagen2026-10-12 12:01 UTC48
        } else {
8b8e83329a1datagen2026-10-12 12:01 UTC49
            RUPTURE_LINE
8b8e83329a1datagen2026-10-12 12:01 UTC50
        };
8b8e83329a1datagen2026-10-12 12:01 UTC51
8b8e83329a1datagen2026-10-12 12:01 UTC52
        if natural == 1 {
8b8e83329a1datagen2026-10-12 12:01 UTC53
            return Err(timed(self.critical_failure(training, &profile)));
8b8e83329a1datagen2026-10-12 12:01 UTC54
        }
8b8e83329a1datagen2026-10-12 12:01 UTC55
        self.here.warm_arrival = false;
8b8e83329a1datagen2026-10-12 12:01 UTC56
        if natural == 2 && self.hotter_than(DANGER_EQ) {
8b8e83329a1datagen2026-10-12 12:01 UTC57
            return Err(timed(self.rupture()));
8b8e83329a1datagen2026-10-12 12:01 UTC58
        }
8b8e83329a1datagen2026-10-12 12:01 UTC59
        if self.hotter_than(rupture_line) && total < dc {
8b8e83329a1datagen2026-10-12 12:01 UTC60
            return Err(timed(self.rupture()));
8b8e83329a1datagen2026-10-12 12:01 UTC61
        }
8b8e83329a1datagen2026-10-12 12:01 UTC62
        if natural == 20 {
97c05b741d8fatagen2026-10-13 11:27 UTC63
            let foreskin = self.gain(length * 2.0, girth * 2.0, profile.foreskin_growth);
8b8e83329a1datagen2026-10-12 12:01 UTC64
            return Ok(Outcome::json(
8b8e83329a1datagen2026-10-12 12:01 UTC65
                StatusCode::PERMANENT_REDIRECT,
97c05b741d8fatagen2026-10-13 11:27 UTC66
                gains(
97c05b741d8fatagen2026-10-13 11:27 UTC67
                    "Your length has moved permanently.",
97c05b741d8fatagen2026-10-13 11:27 UTC68
                    length * 2.0,
97c05b741d8fatagen2026-10-13 11:27 UTC69
                    girth * 2.0,
97c05b741d8fatagen2026-10-13 11:27 UTC70
                    foreskin,
97c05b741d8fatagen2026-10-13 11:27 UTC71
                ),
8b8e83329a1datagen2026-10-12 12:01 UTC72
            )
8b8e83329a1datagen2026-10-12 12:01 UTC73
            .header(header::LOCATION, self.path("/length"))
8b8e83329a1datagen2026-10-12 12:01 UTC74
            .header(SERVER_TIMING, timing));
8b8e83329a1datagen2026-10-12 12:01 UTC75
        }
8b8e83329a1datagen2026-10-12 12:01 UTC76
        if total < dc - NEAR_MISS {
8b8e83329a1datagen2026-10-12 12:01 UTC77
            return Err(timed(not_extended()));
8b8e83329a1datagen2026-10-12 12:01 UTC78
        }
8b8e83329a1datagen2026-10-12 12:01 UTC79
        if total < dc {
8b8e83329a1datagen2026-10-12 12:01 UTC80
            return Ok(Outcome::empty(StatusCode::NO_CONTENT).header(SERVER_TIMING, timing));
8b8e83329a1datagen2026-10-12 12:01 UTC81
        }
8b8e83329a1datagen2026-10-12 12:01 UTC82
        Ok(self
8b8e83329a1datagen2026-10-12 12:01 UTC83
            .land(&profile, length, girth, total >= dc + CLEAN_HIT)
8b8e83329a1datagen2026-10-12 12:01 UTC84
            .header(SERVER_TIMING, timing))
8b8e83329a1datagen2026-10-12 12:01 UTC85
    }
8b8e83329a1datagen2026-10-12 12:01 UTC86
8b8e83329a1datagen2026-10-12 12:01 UTC87
    fn modifier(&self, training: Training, lube: Option<Lube>) -> i32 {
8b8e83329a1datagen2026-10-12 12:01 UTC88
        let band = if EQ_BAND.contains(&self.body.eq) {
8b8e83329a1datagen2026-10-12 12:01 UTC89
            BAND_BONUS
8b8e83329a1datagen2026-10-12 12:01 UTC90
        } else {
8b8e83329a1datagen2026-10-12 12:01 UTC91
            0
8b8e83329a1datagen2026-10-12 12:01 UTC92
        };
8b8e83329a1datagen2026-10-12 12:01 UTC93
        let grip = match (self.body.grip, training) {
8b8e83329a1datagen2026-10-12 12:01 UTC94
            (Grip::Underhand, Training::Stretch) => UNDERHAND_STRETCH,
8b8e83329a1datagen2026-10-12 12:01 UTC95
            (Grip::Underhand, Training::Wet) => UNDERHAND_WET,
8b8e83329a1datagen2026-10-12 12:01 UTC96
            _ => 0,
8b8e83329a1datagen2026-10-12 12:01 UTC97
        };
8b8e83329a1datagen2026-10-12 12:01 UTC98
        let arrival = if self.here.warm_arrival {
8b8e83329a1datagen2026-10-12 12:01 UTC99
            ARRIVAL_BONUS
8b8e83329a1datagen2026-10-12 12:01 UTC100
        } else {
8b8e83329a1datagen2026-10-12 12:01 UTC101
            0
8b8e83329a1datagen2026-10-12 12:01 UTC102
        };
8b8e83329a1datagen2026-10-12 12:01 UTC103
        lube.map_or(0, Lube::modifier) + band + grip + arrival
8b8e83329a1datagen2026-10-12 12:01 UTC104
    }
8b8e83329a1datagen2026-10-12 12:01 UTC105
8b8e83329a1datagen2026-10-12 12:01 UTC106
    fn land(&mut self, profile: &Profile, length: f64, girth: f64, clean: bool) -> Outcome {
8b8e83329a1datagen2026-10-12 12:01 UTC107
        if self.body.eq < *EQ_BAND.start() {
97c05b741d8fatagen2026-10-13 11:27 UTC108
            let foreskin = self.gain(length / 2.0, girth / 2.0, profile.foreskin_growth);
8b8e83329a1datagen2026-10-12 12:01 UTC109
            Outcome::json(
8b8e83329a1datagen2026-10-12 12:01 UTC110
                StatusCode::PARTIAL_CONTENT,
97c05b741d8fatagen2026-10-13 11:27 UTC111
                gains(
97c05b741d8fatagen2026-10-13 11:27 UTC112
                    "Partial gains were applied. EQ was below the band.",
97c05b741d8fatagen2026-10-13 11:27 UTC113
                    length / 2.0,
97c05b741d8fatagen2026-10-13 11:27 UTC114
                    girth / 2.0,
97c05b741d8fatagen2026-10-13 11:27 UTC115
                    foreskin,
97c05b741d8fatagen2026-10-13 11:27 UTC116
                ),
8b8e83329a1datagen2026-10-12 12:01 UTC117
            )
8b8e83329a1datagen2026-10-12 12:01 UTC118
        } else if clean {
97c05b741d8fatagen2026-10-13 11:27 UTC119
            let foreskin = self.gain(length, girth, profile.foreskin_growth);
8b8e83329a1datagen2026-10-12 12:01 UTC120
            Outcome::json(
8b8e83329a1datagen2026-10-12 12:01 UTC121
                StatusCode::OK,
97c05b741d8fatagen2026-10-13 11:27 UTC122
                gains("Gains were applied.", length, girth, foreskin),
8b8e83329a1datagen2026-10-12 12:01 UTC123
            )
8b8e83329a1datagen2026-10-12 12:01 UTC124
        } else {
8b8e83329a1datagen2026-10-12 12:01 UTC125
            let eta = self.oracle.span_int(
8b8e83329a1datagen2026-10-12 12:01 UTC126
                self.decision("eta", "In how many turns will the queued gains arrive?"),
8b8e83329a1datagen2026-10-12 12:01 UTC127
                *QUEUED_ETA.start(),
8b8e83329a1datagen2026-10-12 12:01 UTC128
                *QUEUED_ETA.end(),
8b8e83329a1datagen2026-10-12 12:01 UTC129
            );
8b8e83329a1datagen2026-10-12 12:01 UTC130
            self.body.pending.push(Pending {
8b8e83329a1datagen2026-10-12 12:01 UTC131
                due: self.turn + eta.unsigned_abs(),
8b8e83329a1datagen2026-10-12 12:01 UTC132
                length,
8b8e83329a1datagen2026-10-12 12:01 UTC133
                girth,
8b8e83329a1datagen2026-10-12 12:01 UTC134
                foreskin: profile.foreskin_growth,
8b8e83329a1datagen2026-10-12 12:01 UTC135
            });
6033b0db186catagen2026-10-13 12:44 UTC136
            let mut body = json!({
6033b0db186catagen2026-10-13 12:44 UTC137
                "detail": "Gains have been queued. They may arrive.",
6033b0db186catagen2026-10-13 12:44 UTC138
                "eta_turns": eta,
6033b0db186catagen2026-10-13 12:44 UTC139
            });
6033b0db186catagen2026-10-13 12:44 UTC140
            let torn = std::mem::take(&mut self.body.torn);
6033b0db186catagen2026-10-13 12:44 UTC141
            if torn > 0.0 {
6033b0db186catagen2026-10-13 12:44 UTC142
                body["foreskin"] = json!(-round3(torn));
6033b0db186catagen2026-10-13 12:44 UTC143
            }
6033b0db186catagen2026-10-13 12:44 UTC144
            Outcome::json(StatusCode::ACCEPTED, body)
8b8e83329a1datagen2026-10-12 12:01 UTC145
        }
8b8e83329a1datagen2026-10-12 12:01 UTC146
    }
8b8e83329a1datagen2026-10-12 12:01 UTC147
8b8e83329a1datagen2026-10-12 12:01 UTC148
    pub(super) fn scaled_gains(&self, profile: &Profile, intensity: i32) -> (f64, f64) {
8b8e83329a1datagen2026-10-12 12:01 UTC149
        let scale = f64::from(intensity);
8b8e83329a1datagen2026-10-12 12:01 UTC150
        let (mut length, mut girth) = (profile.length * scale, profile.girth * scale);
8b8e83329a1datagen2026-10-12 12:01 UTC151
        let greed = if self.body.eq > HOT_EQ {
8b8e83329a1datagen2026-10-12 12:01 UTC152
            GREED_HOT
8b8e83329a1datagen2026-10-12 12:01 UTC153
        } else if self.body.eq > DANGER_EQ {
8b8e83329a1datagen2026-10-12 12:01 UTC154
            GREED_WARM
8b8e83329a1datagen2026-10-12 12:01 UTC155
        } else {
8b8e83329a1datagen2026-10-12 12:01 UTC156
            1.0
8b8e83329a1datagen2026-10-12 12:01 UTC157
        };
8b8e83329a1datagen2026-10-12 12:01 UTC158
        length *= greed;
8b8e83329a1datagen2026-10-12 12:01 UTC159
        girth *= greed;
8b8e83329a1datagen2026-10-12 12:01 UTC160
        if self.here.teapot {
8b8e83329a1datagen2026-10-12 12:01 UTC161
            length *= 0.5;
8b8e83329a1datagen2026-10-12 12:01 UTC162
            girth *= 1.5;
8b8e83329a1datagen2026-10-12 12:01 UTC163
        }
8b8e83329a1datagen2026-10-12 12:01 UTC164
        if self.items.kit.ring {
8b8e83329a1datagen2026-10-12 12:01 UTC165
            girth *= 2.0;
8b8e83329a1datagen2026-10-12 12:01 UTC166
        }
8b8e83329a1datagen2026-10-12 12:01 UTC167
        if self.body.conditions.friction_burn {
8b8e83329a1datagen2026-10-12 12:01 UTC168
            length *= 0.5;
8b8e83329a1datagen2026-10-12 12:01 UTC169
            girth *= 0.5;
8b8e83329a1datagen2026-10-12 12:01 UTC170
        }
8b8e83329a1datagen2026-10-12 12:01 UTC171
        (length, girth)
8b8e83329a1datagen2026-10-12 12:01 UTC172
    }
8b8e83329a1datagen2026-10-12 12:01 UTC173
8b8e83329a1datagen2026-10-12 12:01 UTC174
    pub(super) fn critical_failure(&mut self, training: Training, profile: &Profile) -> Problem {
8b8e83329a1datagen2026-10-12 12:01 UTC175
        let uses_foreskin = profile.foreskin.is_some_and(|(min, _)| min > 0.0);
97c05b741d8fatagen2026-10-13 11:27 UTC176
        if uses_foreskin && let Some(lost) = self.body.foreskin.take() {
8b8e83329a1datagen2026-10-12 12:01 UTC177
            self.standing.shame += 25;
8b8e83329a1datagen2026-10-12 12:01 UTC178
            return Problem::new(
8b8e83329a1datagen2026-10-12 12:01 UTC179
                StatusCode::GONE,
8b8e83329a1datagen2026-10-12 12:01 UTC180
                "foreskin-lost",
8b8e83329a1datagen2026-10-12 12:01 UTC181
                "The technique has removed the foreskin. It will not be returning.",
8b8e83329a1datagen2026-10-12 12:01 UTC182
            )
97c05b741d8fatagen2026-10-13 11:27 UTC183
            .with("foreskin", -round3(lost))
8b8e83329a1datagen2026-10-12 12:01 UTC184
            .header(header::CONTENT_LOCATION, self.path("/foreskin"));
8b8e83329a1datagen2026-10-12 12:01 UTC185
        }
8b8e83329a1datagen2026-10-12 12:01 UTC186
        if self.hotter_than(DANGER_EQ) {
8b8e83329a1datagen2026-10-12 12:01 UTC187
            return self.rupture();
8b8e83329a1datagen2026-10-12 12:01 UTC188
        }
8b8e83329a1datagen2026-10-12 12:01 UTC189
        if training == Training::Stretch && !self.here.teapot {
8b8e83329a1datagen2026-10-12 12:01 UTC190
            self.here.teapot = true;
8b8e83329a1datagen2026-10-12 12:01 UTC191
            self.items.kit.teapot_spout = true;
8b8e83329a1datagen2026-10-12 12:01 UTC192
            self.standing.shame += 5;
8b8e83329a1datagen2026-10-12 12:01 UTC193
            return Problem::new(
8b8e83329a1datagen2026-10-12 12:01 UTC194
                StatusCode::IM_A_TEAPOT,
8b8e83329a1datagen2026-10-12 12:01 UTC195
                "teapot",
8b8e83329a1datagen2026-10-12 12:01 UTC196
                "The subject is now short and stout. Length gains are halved for the rest of the floor.",
8b8e83329a1datagen2026-10-12 12:01 UTC197
            );
8b8e83329a1datagen2026-10-12 12:01 UTC198
        }
8b8e83329a1datagen2026-10-12 12:01 UTC199
        if training == Training::Uli && !self.body.conditions.peyronie {
8b8e83329a1datagen2026-10-12 12:01 UTC200
            self.body.conditions.peyronie = true;
8b8e83329a1datagen2026-10-12 12:01 UTC201
            self.standing.shame += 5;
8b8e83329a1datagen2026-10-12 12:01 UTC202
            return Problem::new(
8b8e83329a1datagen2026-10-12 12:01 UTC203
                StatusCode::MISDIRECTED_REQUEST,
8b8e83329a1datagen2026-10-12 12:01 UTC204
                "curvature",
8b8e83329a1datagen2026-10-12 12:01 UTC205
                "A curvature has developed. Techniques will land slightly to the left until a professional is consulted.",
8b8e83329a1datagen2026-10-12 12:01 UTC206
            );
8b8e83329a1datagen2026-10-12 12:01 UTC207
        }
8b8e83329a1datagen2026-10-12 12:01 UTC208
        not_extended()
8b8e83329a1datagen2026-10-12 12:01 UTC209
    }
8b8e83329a1datagen2026-10-12 12:01 UTC210
8b8e83329a1datagen2026-10-12 12:01 UTC211
    pub(super) fn gossip(&mut self, outcome: Outcome) -> Outcome {
8b8e83329a1datagen2026-10-12 12:01 UTC212
        let reportable = matches!(outcome.status().as_u16(), 200 | 202 | 206 | 308);
8b8e83329a1datagen2026-10-12 12:01 UTC213
        if !self.here.brother_told || !reportable {
8b8e83329a1datagen2026-10-12 12:01 UTC214
            return outcome;
8b8e83329a1datagen2026-10-12 12:01 UTC215
        }
8b8e83329a1datagen2026-10-12 12:01 UTC216
        self.here.brother_told = false;
8b8e83329a1datagen2026-10-12 12:01 UTC217
        outcome.retold(
8b8e83329a1datagen2026-10-12 12:01 UTC218
            StatusCode::ALREADY_REPORTED,
8b8e83329a1datagen2026-10-12 12:01 UTC219
            "Your brother has already told Mum.",
8b8e83329a1datagen2026-10-12 12:01 UTC220
        )
8b8e83329a1datagen2026-10-12 12:01 UTC221
    }
8b8e83329a1datagen2026-10-12 12:01 UTC222
}
8b8e83329a1datagen2026-10-12 12:01 UTC223
8b8e83329a1datagen2026-10-12 12:01 UTC224
pub(super) fn not_extended() -> Problem {
8b8e83329a1datagen2026-10-12 12:01 UTC225
    Problem::new(
8b8e83329a1datagen2026-10-12 12:01 UTC226
        StatusCode::NOT_EXTENDED,
8b8e83329a1datagen2026-10-12 12:01 UTC227
        "not-extended",
8b8e83329a1datagen2026-10-12 12:01 UTC228
        "The requested extension was not applied. The server has recorded the attempt.",
8b8e83329a1datagen2026-10-12 12:01 UTC229
    )
8b8e83329a1datagen2026-10-12 12:01 UTC230
}