atagen

jelq

public

an http based roguelike rpg about jelqing

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