atagen

jelq

public

an http based roguelike rpg about jelqing

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