atagen

jelq

public

an http based roguelike rpg about jelqing

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

Blame: src/game/turn.rs

782069860236atagen2026-10-10 12:44 UTC1
use std::{fmt::Write as _, ops::ControlFlow, time::Instant};
782069860236atagen2026-10-10 12:44 UTC2
782069860236atagen2026-10-10 12:44 UTC3
use axum::http::{StatusCode, header};
782069860236atagen2026-10-10 12:44 UTC4
use rand::Rng;
782069860236atagen2026-10-10 12:44 UTC5
use serde_json::json;
782069860236atagen2026-10-10 12:44 UTC6
782069860236atagen2026-10-10 12:44 UTC7
use super::{
782069860236atagen2026-10-10 12:44 UTC8
    Floor, Grip, LUBRICANTS, Lube, Outcome, Payload, Run, Story, Technique,
782069860236atagen2026-10-10 12:44 UTC9
    balance::{
9683736a2c5eatagen2026-10-10 13:15 UTC10
        BAND_BONUS, BRANCH_TURN, CAT_TAKES_TOWEL, CHANGING_ROOM_CHILL, DANGER_EQ, EQ_BAND,
9683736a2c5eatagen2026-10-10 13:15 UTC11
        EQ_WOBBLE, EXTENDER_GAIN, HOT_EQ, OBSERVED, PRIAPISM_EQ, PRIAPISM_TURNS, PULLED_MUSCLE,
782069860236atagen2026-10-10 12:44 UTC12
        PULLED_MUSCLE_INTENSITY, RING_PRIAPISM, RING_RUPTURE_LINE, RUPTURE_LINE, STORAGE_GIRTH,
782069860236atagen2026-10-10 12:44 UTC13
        SUSPICION_LIMIT,
782069860236atagen2026-10-10 12:44 UTC14
    },
782069860236atagen2026-10-10 12:44 UTC15
    not_extended, round3,
9683736a2c5eatagen2026-10-10 13:15 UTC16
    state::{MumVisit, Observer, Pending},
782069860236atagen2026-10-10 12:44 UTC17
    techniques::{LubeRule, Profile, Training, Utility},
782069860236atagen2026-10-10 12:44 UTC18
};
782069860236atagen2026-10-10 12:44 UTC19
use crate::{headers::SERVER_TIMING, problem::Problem};
782069860236atagen2026-10-10 12:44 UTC20
782069860236atagen2026-10-10 12:44 UTC21
impl Run {
782069860236atagen2026-10-10 12:44 UTC22
    pub fn jelq(
782069860236atagen2026-10-10 12:44 UTC23
        &mut self,
782069860236atagen2026-10-10 12:44 UTC24
        techniques: &[Technique],
782069860236atagen2026-10-10 12:44 UTC25
        intensity: i32,
782069860236atagen2026-10-10 12:44 UTC26
        lube: Option<Lube>,
782069860236atagen2026-10-10 12:44 UTC27
        now: Instant,
782069860236atagen2026-10-10 12:44 UTC28
    ) -> Result<Outcome, Problem> {
782069860236atagen2026-10-10 12:44 UTC29
        self.check_refractory(now)?;
782069860236atagen2026-10-10 12:44 UTC30
        if self.story == Story::Pending {
782069860236atagen2026-10-10 12:44 UTC31
            return Ok(self.branch_offer());
782069860236atagen2026-10-10 12:44 UTC32
        }
782069860236atagen2026-10-10 12:44 UTC33
        if self.floor == Floor::Forum {
782069860236atagen2026-10-10 12:44 UTC34
            return Ok(Outcome::json(
782069860236atagen2026-10-10 12:44 UTC35
                StatusCode::SEE_OTHER,
782069860236atagen2026-10-10 12:44 UTC36
                json!({ "detail": "Refer to the forum." }),
782069860236atagen2026-10-10 12:44 UTC37
            )
782069860236atagen2026-10-10 12:44 UTC38
            .header(header::LOCATION, super::FORUM_THREAD));
782069860236atagen2026-10-10 12:44 UTC39
        }
782069860236atagen2026-10-10 12:44 UTC40
        if self.items.cage {
782069860236atagen2026-10-10 12:44 UTC41
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC42
                StatusCode::LOCKED,
782069860236atagen2026-10-10 12:44 UTC43
                "caged",
782069860236atagen2026-10-10 12:44 UTC44
                "The cage is locked. The key is not in the box.",
782069860236atagen2026-10-10 12:44 UTC45
            ));
782069860236atagen2026-10-10 12:44 UTC46
        }
782069860236atagen2026-10-10 12:44 UTC47
        match *techniques {
782069860236atagen2026-10-10 12:44 UTC48
            [Technique::Utility(utility)] => self.utility(utility, lube, now),
782069860236atagen2026-10-10 12:44 UTC49
            [Technique::Training(training)] => self.single(training, intensity, lube, now),
782069860236atagen2026-10-10 12:44 UTC50
            [Technique::Training(a), Technique::Training(b)] => {
782069860236atagen2026-10-10 12:44 UTC51
                self.combo(a, b, intensity, lube, now)
782069860236atagen2026-10-10 12:44 UTC52
            }
782069860236atagen2026-10-10 12:44 UTC53
            [_, _] => Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC54
                StatusCode::BAD_REQUEST,
782069860236atagen2026-10-10 12:44 UTC55
                "combo-invalid",
782069860236atagen2026-10-10 12:44 UTC56
                "Only training techniques can be combined.",
782069860236atagen2026-10-10 12:44 UTC57
            )),
782069860236atagen2026-10-10 12:44 UTC58
            _ => Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC59
                StatusCode::BAD_REQUEST,
782069860236atagen2026-10-10 12:44 UTC60
                "too-many-techniques",
782069860236atagen2026-10-10 12:44 UTC61
                "Two techniques at most. The server has limits even if you do not.",
782069860236atagen2026-10-10 12:44 UTC62
            )),
782069860236atagen2026-10-10 12:44 UTC63
        }
782069860236atagen2026-10-10 12:44 UTC64
    }
782069860236atagen2026-10-10 12:44 UTC65
782069860236atagen2026-10-10 12:44 UTC66
    fn utility(
782069860236atagen2026-10-10 12:44 UTC67
        &mut self,
782069860236atagen2026-10-10 12:44 UTC68
        utility: Utility,
782069860236atagen2026-10-10 12:44 UTC69
        lube: Option<Lube>,
782069860236atagen2026-10-10 12:44 UTC70
        now: Instant,
782069860236atagen2026-10-10 12:44 UTC71
    ) -> Result<Outcome, Problem> {
782069860236atagen2026-10-10 12:44 UTC72
        match utility {
782069860236atagen2026-10-10 12:44 UTC73
            Utility::Warmup => return self.warm_up(),
782069860236atagen2026-10-10 12:44 UTC74
            Utility::Ice => return self.ice(),
782069860236atagen2026-10-10 12:44 UTC75
            Utility::Supplement => return self.supplement(),
782069860236atagen2026-10-10 12:44 UTC76
            Utility::Edge | Utility::Pump => {}
782069860236atagen2026-10-10 12:44 UTC77
        }
782069860236atagen2026-10-10 12:44 UTC78
        if utility == Utility::Pump {
782069860236atagen2026-10-10 12:44 UTC79
            self.here.edges = 0;
782069860236atagen2026-10-10 12:44 UTC80
        }
782069860236atagen2026-10-10 12:44 UTC81
        self.ready(Technique::Utility(utility), lube)?;
782069860236atagen2026-10-10 12:44 UTC82
        self.check_storage()?;
782069860236atagen2026-10-10 12:44 UTC83
        self.floor_enemy()?;
782069860236atagen2026-10-10 12:44 UTC84
        match utility {
782069860236atagen2026-10-10 12:44 UTC85
            Utility::Edge => self.edge(now),
782069860236atagen2026-10-10 12:44 UTC86
            _ => self.pump(now),
782069860236atagen2026-10-10 12:44 UTC87
        }
782069860236atagen2026-10-10 12:44 UTC88
    }
782069860236atagen2026-10-10 12:44 UTC89
782069860236atagen2026-10-10 12:44 UTC90
    fn single(
782069860236atagen2026-10-10 12:44 UTC91
        &mut self,
782069860236atagen2026-10-10 12:44 UTC92
        training: Training,
782069860236atagen2026-10-10 12:44 UTC93
        intensity: i32,
782069860236atagen2026-10-10 12:44 UTC94
        lube: Option<Lube>,
782069860236atagen2026-10-10 12:44 UTC95
        now: Instant,
782069860236atagen2026-10-10 12:44 UTC96
    ) -> Result<Outcome, Problem> {
782069860236atagen2026-10-10 12:44 UTC97
        self.here.edges = 0;
782069860236atagen2026-10-10 12:44 UTC98
        self.ready(Technique::Training(training), lube)?;
782069860236atagen2026-10-10 12:44 UTC99
        self.check_storage()?;
782069860236atagen2026-10-10 12:44 UTC100
        self.floor_enemy()?;
782069860236atagen2026-10-10 12:44 UTC101
        self.commit(intensity, lube, now, 1);
782069860236atagen2026-10-10 12:44 UTC102
        if let ControlFlow::Break(interrupted) = self.mishaps(training, intensity)? {
782069860236atagen2026-10-10 12:44 UTC103
            return Ok(interrupted);
782069860236atagen2026-10-10 12:44 UTC104
        }
782069860236atagen2026-10-10 12:44 UTC105
        if let ControlFlow::Break(interrupted) = self.hazards(intensity)? {
782069860236atagen2026-10-10 12:44 UTC106
            return Ok(interrupted);
782069860236atagen2026-10-10 12:44 UTC107
        }
782069860236atagen2026-10-10 12:44 UTC108
        let outcome = self.roll(training, intensity, lube)?;
782069860236atagen2026-10-10 12:44 UTC109
        Ok(self.gossip(outcome))
782069860236atagen2026-10-10 12:44 UTC110
    }
782069860236atagen2026-10-10 12:44 UTC111
782069860236atagen2026-10-10 12:44 UTC112
    fn combo(
782069860236atagen2026-10-10 12:44 UTC113
        &mut self,
782069860236atagen2026-10-10 12:44 UTC114
        a: Training,
782069860236atagen2026-10-10 12:44 UTC115
        b: Training,
782069860236atagen2026-10-10 12:44 UTC116
        intensity: i32,
782069860236atagen2026-10-10 12:44 UTC117
        lube: Option<Lube>,
782069860236atagen2026-10-10 12:44 UTC118
        now: Instant,
782069860236atagen2026-10-10 12:44 UTC119
    ) -> Result<Outcome, Problem> {
782069860236atagen2026-10-10 12:44 UTC120
        if a == b {
782069860236atagen2026-10-10 12:44 UTC121
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC122
                StatusCode::BAD_REQUEST,
782069860236atagen2026-10-10 12:44 UTC123
                "combo-invalid",
782069860236atagen2026-10-10 12:44 UTC124
                "A technique cannot be combined with itself. The server has checked.",
782069860236atagen2026-10-10 12:44 UTC125
            ));
782069860236atagen2026-10-10 12:44 UTC126
        }
782069860236atagen2026-10-10 12:44 UTC127
        self.here.edges = 0;
782069860236atagen2026-10-10 12:44 UTC128
        self.ready(Technique::Training(a), lube)?;
782069860236atagen2026-10-10 12:44 UTC129
        self.ready(Technique::Training(b), lube)?;
782069860236atagen2026-10-10 12:44 UTC130
        self.check_storage()?;
782069860236atagen2026-10-10 12:44 UTC131
        self.floor_enemy()?;
782069860236atagen2026-10-10 12:44 UTC132
        self.commit(intensity, lube, now, 2);
782069860236atagen2026-10-10 12:44 UTC133
        for training in [a, b] {
782069860236atagen2026-10-10 12:44 UTC134
            if let ControlFlow::Break(interrupted) = self.mishaps(training, intensity)? {
782069860236atagen2026-10-10 12:44 UTC135
                return Ok(interrupted);
782069860236atagen2026-10-10 12:44 UTC136
            }
782069860236atagen2026-10-10 12:44 UTC137
        }
782069860236atagen2026-10-10 12:44 UTC138
        if let ControlFlow::Break(interrupted) = self.hazards(intensity)? {
782069860236atagen2026-10-10 12:44 UTC139
            return Ok(interrupted);
782069860236atagen2026-10-10 12:44 UTC140
        }
782069860236atagen2026-10-10 12:44 UTC141
782069860236atagen2026-10-10 12:44 UTC142
        let mut statuses = Vec::new();
782069860236atagen2026-10-10 12:44 UTC143
        for training in [a, b] {
782069860236atagen2026-10-10 12:44 UTC144
            let result = self.roll(training, intensity, lube);
782069860236atagen2026-10-10 12:44 UTC145
            if self.ended.is_some() {
782069860236atagen2026-10-10 12:44 UTC146
                return result;
782069860236atagen2026-10-10 12:44 UTC147
            }
782069860236atagen2026-10-10 12:44 UTC148
            let status = match result {
782069860236atagen2026-10-10 12:44 UTC149
                Ok(outcome) => self.gossip(outcome).status,
782069860236atagen2026-10-10 12:44 UTC150
                Err(problem) => problem.status(),
782069860236atagen2026-10-10 12:44 UTC151
            };
782069860236atagen2026-10-10 12:44 UTC152
            statuses.push((training, status));
782069860236atagen2026-10-10 12:44 UTC153
        }
782069860236atagen2026-10-10 12:44 UTC154
782069860236atagen2026-10-10 12:44 UTC155
        let mut xml = String::from(
782069860236atagen2026-10-10 12:44 UTC156
            "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<D:multistatus xmlns:D=\"DAV:\">\n",
782069860236atagen2026-10-10 12:44 UTC157
        );
782069860236atagen2026-10-10 12:44 UTC158
        for (training, status) in statuses {
782069860236atagen2026-10-10 12:44 UTC159
            let _ = write!(
782069860236atagen2026-10-10 12:44 UTC160
                xml,
782069860236atagen2026-10-10 12:44 UTC161
                "  <D:response>\n    <D:href>{}?technique={}</D:href>\n    <D:status>HTTP/1.1 {} {}</D:status>\n  </D:response>\n",
782069860236atagen2026-10-10 12:44 UTC162
                self.path(""),
782069860236atagen2026-10-10 12:44 UTC163
                training.name(),
782069860236atagen2026-10-10 12:44 UTC164
                status.as_u16(),
782069860236atagen2026-10-10 12:44 UTC165
                crate::problem::reason(status),
782069860236atagen2026-10-10 12:44 UTC166
            );
782069860236atagen2026-10-10 12:44 UTC167
        }
782069860236atagen2026-10-10 12:44 UTC168
        xml.push_str(
782069860236atagen2026-10-10 12:44 UTC169
            "  <D:responsedescription>Each technique has been reported separately.</D:responsedescription>\n</D:multistatus>\n",
782069860236atagen2026-10-10 12:44 UTC170
        );
782069860236atagen2026-10-10 12:44 UTC171
        Ok(Outcome::raw(
782069860236atagen2026-10-10 12:44 UTC172
            StatusCode::MULTI_STATUS,
782069860236atagen2026-10-10 12:44 UTC173
            "application/xml; charset=utf-8",
782069860236atagen2026-10-10 12:44 UTC174
            xml,
782069860236atagen2026-10-10 12:44 UTC175
        ))
782069860236atagen2026-10-10 12:44 UTC176
    }
782069860236atagen2026-10-10 12:44 UTC177
782069860236atagen2026-10-10 12:44 UTC178
    fn ready(&self, technique: Technique, lube: Option<Lube>) -> Result<(), Problem> {
782069860236atagen2026-10-10 12:44 UTC179
        if technique == Technique::Training(Training::Uli) && !self.forum_read() {
782069860236atagen2026-10-10 12:44 UTC180
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC181
                StatusCode::FORBIDDEN,
782069860236atagen2026-10-10 12:44 UTC182
                "technique-locked",
782069860236atagen2026-10-10 12:44 UTC183
                "This technique is discussed on the forum. You have not been to the forum.",
782069860236atagen2026-10-10 12:44 UTC184
            ));
782069860236atagen2026-10-10 12:44 UTC185
        }
782069860236atagen2026-10-10 12:44 UTC186
        if !self.warmed_up {
782069860236atagen2026-10-10 12:44 UTC187
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC188
                StatusCode::PRECONDITION_FAILED,
782069860236atagen2026-10-10 12:44 UTC189
                "cold-start",
782069860236atagen2026-10-10 12:44 UTC190
                "Warm-up was skipped. The server has noted this.",
782069860236atagen2026-10-10 12:44 UTC191
            )
782069860236atagen2026-10-10 12:44 UTC192
            .with("technique", Utility::Warmup.name()));
782069860236atagen2026-10-10 12:44 UTC193
        }
782069860236atagen2026-10-10 12:44 UTC194
        let training = match technique {
782069860236atagen2026-10-10 12:44 UTC195
            Technique::Utility(Utility::Pump) if !self.items.pump_pro => {
782069860236atagen2026-10-10 12:44 UTC196
                return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC197
                    StatusCode::FAILED_DEPENDENCY,
782069860236atagen2026-10-10 12:44 UTC198
                    "no-pump",
782069860236atagen2026-10-10 12:44 UTC199
                    "The pump technique depends on a pump.",
782069860236atagen2026-10-10 12:44 UTC200
                ));
782069860236atagen2026-10-10 12:44 UTC201
            }
782069860236atagen2026-10-10 12:44 UTC202
            Technique::Utility(_) => return Ok(()),
782069860236atagen2026-10-10 12:44 UTC203
            Technique::Training(training) => training,
782069860236atagen2026-10-10 12:44 UTC204
        };
782069860236atagen2026-10-10 12:44 UTC205
        let profile = training.profile();
782069860236atagen2026-10-10 12:44 UTC206
        match (profile.lube, lube) {
782069860236atagen2026-10-10 12:44 UTC207
            (LubeRule::Required, None) => {
782069860236atagen2026-10-10 12:44 UTC208
                return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC209
                    StatusCode::UNSUPPORTED_MEDIA_TYPE,
782069860236atagen2026-10-10 12:44 UTC210
                    "lubricant-required",
782069860236atagen2026-10-10 12:44 UTC211
                    format!(
782069860236atagen2026-10-10 12:44 UTC212
                        "The {} technique requires a lubricant. None was declared.",
782069860236atagen2026-10-10 12:44 UTC213
                        training.name()
782069860236atagen2026-10-10 12:44 UTC214
                    ),
782069860236atagen2026-10-10 12:44 UTC215
                )
782069860236atagen2026-10-10 12:44 UTC216
                .with("lubricants", json!(LUBRICANTS)));
782069860236atagen2026-10-10 12:44 UTC217
            }
782069860236atagen2026-10-10 12:44 UTC218
            (LubeRule::Forbidden, Some(_)) => {
782069860236atagen2026-10-10 12:44 UTC219
                return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC220
                    StatusCode::UNSUPPORTED_MEDIA_TYPE,
782069860236atagen2026-10-10 12:44 UTC221
                    "lubricant-declared",
782069860236atagen2026-10-10 12:44 UTC222
                    format!(
782069860236atagen2026-10-10 12:44 UTC223
                        "The {} technique was requested. A lubricant was declared.",
782069860236atagen2026-10-10 12:44 UTC224
                        training.name()
782069860236atagen2026-10-10 12:44 UTC225
                    ),
782069860236atagen2026-10-10 12:44 UTC226
                ));
782069860236atagen2026-10-10 12:44 UTC227
            }
782069860236atagen2026-10-10 12:44 UTC228
            _ => {}
782069860236atagen2026-10-10 12:44 UTC229
        }
782069860236atagen2026-10-10 12:44 UTC230
        match profile.foreskin {
782069860236atagen2026-10-10 12:44 UTC231
            Some((min, max)) => self.check_foreskin(min, max),
782069860236atagen2026-10-10 12:44 UTC232
            None => Ok(()),
782069860236atagen2026-10-10 12:44 UTC233
        }
782069860236atagen2026-10-10 12:44 UTC234
    }
782069860236atagen2026-10-10 12:44 UTC235
782069860236atagen2026-10-10 12:44 UTC236
    fn check_foreskin(&self, min: f64, max: f64) -> Result<(), Problem> {
782069860236atagen2026-10-10 12:44 UTC237
        let Some(f) = self.foreskin else {
782069860236atagen2026-10-10 12:44 UTC238
            if min <= 0.0 {
782069860236atagen2026-10-10 12:44 UTC239
                return Ok(());
782069860236atagen2026-10-10 12:44 UTC240
            }
782069860236atagen2026-10-10 12:44 UTC241
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC242
                StatusCode::GONE,
782069860236atagen2026-10-10 12:44 UTC243
                "foreskin-gone",
782069860236atagen2026-10-10 12:44 UTC244
                "This technique requires a foreskin. The foreskin is gone.",
782069860236atagen2026-10-10 12:44 UTC245
            )
782069860236atagen2026-10-10 12:44 UTC246
            .header(header::CONTENT_LOCATION, self.path("/foreskin")));
782069860236atagen2026-10-10 12:44 UTC247
        };
782069860236atagen2026-10-10 12:44 UTC248
        if f < min {
782069860236atagen2026-10-10 12:44 UTC249
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC250
                StatusCode::LENGTH_REQUIRED,
782069860236atagen2026-10-10 12:44 UTC251
                "foreskin-insufficient",
782069860236atagen2026-10-10 12:44 UTC252
                format!(
782069860236atagen2026-10-10 12:44 UTC253
                    "The selected technique requires {min:.1} units of foreskin. {f:.2} units are available. The server has noted the shortfall."
782069860236atagen2026-10-10 12:44 UTC254
                ),
782069860236atagen2026-10-10 12:44 UTC255
            )
782069860236atagen2026-10-10 12:44 UTC256
            .with("required", min));
782069860236atagen2026-10-10 12:44 UTC257
        }
782069860236atagen2026-10-10 12:44 UTC258
        if f > max {
782069860236atagen2026-10-10 12:44 UTC259
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC260
                StatusCode::PAYLOAD_TOO_LARGE,
782069860236atagen2026-10-10 12:44 UTC261
                "foreskin-excessive",
782069860236atagen2026-10-10 12:44 UTC262
                format!(
782069860236atagen2026-10-10 12:44 UTC263
                    "The selected technique tolerates at most {max:.1} units of foreskin. {f:.2} units are present."
782069860236atagen2026-10-10 12:44 UTC264
                ),
782069860236atagen2026-10-10 12:44 UTC265
            )
782069860236atagen2026-10-10 12:44 UTC266
            .with("maximum", max));
782069860236atagen2026-10-10 12:44 UTC267
        }
782069860236atagen2026-10-10 12:44 UTC268
        Ok(())
782069860236atagen2026-10-10 12:44 UTC269
    }
782069860236atagen2026-10-10 12:44 UTC270
782069860236atagen2026-10-10 12:44 UTC271
    fn check_storage(&self) -> Result<(), Problem> {
782069860236atagen2026-10-10 12:44 UTC272
        if self.items.extender && self.girth > STORAGE_GIRTH {
782069860236atagen2026-10-10 12:44 UTC273
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC274
                StatusCode::INSUFFICIENT_STORAGE,
782069860236atagen2026-10-10 12:44 UTC275
                "jeans-full",
782069860236atagen2026-10-10 12:44 UTC276
                "The jeans are full. The extender is occupying the remaining storage.",
782069860236atagen2026-10-10 12:44 UTC277
            )
782069860236atagen2026-10-10 12:44 UTC278
            .with("extender", self.path("/inventory/extender")));
782069860236atagen2026-10-10 12:44 UTC279
        }
782069860236atagen2026-10-10 12:44 UTC280
        Ok(())
782069860236atagen2026-10-10 12:44 UTC281
    }
782069860236atagen2026-10-10 12:44 UTC282
782069860236atagen2026-10-10 12:44 UTC283
    pub(super) fn commit(&mut self, intensity: i32, lube: Option<Lube>, now: Instant, cost: i32) {
782069860236atagen2026-10-10 12:44 UTC284
        self.touch();
782069860236atagen2026-10-10 12:44 UTC285
        self.turn += 1;
782069860236atagen2026-10-10 12:44 UTC286
        self.here.turns += 1;
9683736a2c5eatagen2026-10-10 13:15 UTC287
        self.realise();
782069860236atagen2026-10-10 12:44 UTC288
        if self.items.extender {
782069860236atagen2026-10-10 12:44 UTC289
            self.length += EXTENDER_GAIN;
782069860236atagen2026-10-10 12:44 UTC290
        }
782069860236atagen2026-10-10 12:44 UTC291
        self.stamina -= cost + i32::from(lube == Some(Lube::Lotion));
782069860236atagen2026-10-10 12:44 UTC292
        if self.stamina <= 0 {
782069860236atagen2026-10-10 12:44 UTC293
            self.exhaust(now);
782069860236atagen2026-10-10 12:44 UTC294
        }
9683736a2c5eatagen2026-10-10 13:15 UTC295
        let chill = if self.floor == Floor::ChangingRoom {
9683736a2c5eatagen2026-10-10 13:15 UTC296
            CHANGING_ROOM_CHILL
782069860236atagen2026-10-10 12:44 UTC297
        } else {
782069860236atagen2026-10-10 12:44 UTC298
            0
782069860236atagen2026-10-10 12:44 UTC299
        };
782069860236atagen2026-10-10 12:44 UTC300
        let wobble = self.rng.random_range(-EQ_WOBBLE..=EQ_WOBBLE);
782069860236atagen2026-10-10 12:44 UTC301
        self.eq = (self.eq + intensity * 2 - 6 - chill + wobble).clamp(0, 100);
782069860236atagen2026-10-10 12:44 UTC302
782069860236atagen2026-10-10 12:44 UTC303
        let mut suspicion = intensity / 5 + i32::from(lube == Some(Lube::Vaseline));
9683736a2c5eatagen2026-10-10 13:15 UTC304
        if matches!(self.floor, Floor::ChangingRoom | Floor::Nans) {
782069860236atagen2026-10-10 12:44 UTC305
            suspicion *= 2;
782069860236atagen2026-10-10 12:44 UTC306
        }
9683736a2c5eatagen2026-10-10 13:15 UTC307
        if self.floor == Floor::Nans && lube == Some(Lube::CoconutOil) {
782069860236atagen2026-10-10 12:44 UTC308
            suspicion += 2;
782069860236atagen2026-10-10 12:44 UTC309
        }
782069860236atagen2026-10-10 12:44 UTC310
        let was_below = self.suspicion < SUSPICION_LIMIT;
782069860236atagen2026-10-10 12:44 UTC311
        self.suspicion =
782069860236atagen2026-10-10 12:44 UTC312
            u8::try_from((i32::from(self.suspicion) + suspicion).min(i32::from(SUSPICION_LIMIT)))
782069860236atagen2026-10-10 12:44 UTC313
                .unwrap_or(SUSPICION_LIMIT);
9683736a2c5eatagen2026-10-10 13:15 UTC314
        self.maybe_summon_mum(was_below);
782069860236atagen2026-10-10 12:44 UTC315
782069860236atagen2026-10-10 12:44 UTC316
        if let Some(observer) = self.floor.observer()
782069860236atagen2026-10-10 12:44 UTC317
            && self.chance(OBSERVED)
782069860236atagen2026-10-10 12:44 UTC318
        {
782069860236atagen2026-10-10 12:44 UTC319
            self.observed = Some(observer);
782069860236atagen2026-10-10 12:44 UTC320
            self.shame += 1;
782069860236atagen2026-10-10 12:44 UTC321
            if observer == Observer::Cat && self.chance(CAT_TAKES_TOWEL) {
782069860236atagen2026-10-10 12:44 UTC322
                self.here.towel_stolen = true;
782069860236atagen2026-10-10 12:44 UTC323
            }
782069860236atagen2026-10-10 12:44 UTC324
        }
782069860236atagen2026-10-10 12:44 UTC325
        if intensity >= PULLED_MUSCLE_INTENSITY
782069860236atagen2026-10-10 12:44 UTC326
            && !self.conditions.pulled_muscle
782069860236atagen2026-10-10 12:44 UTC327
            && self.chance(PULLED_MUSCLE)
782069860236atagen2026-10-10 12:44 UTC328
        {
782069860236atagen2026-10-10 12:44 UTC329
            self.conditions.pulled_muscle = true;
782069860236atagen2026-10-10 12:44 UTC330
            self.stamina = self.stamina.min(self.max_stamina());
782069860236atagen2026-10-10 12:44 UTC331
        }
782069860236atagen2026-10-10 12:44 UTC332
        if self.items.ring && self.eq > HOT_EQ && self.chance(RING_PRIAPISM) {
782069860236atagen2026-10-10 12:44 UTC333
            self.conditions.priapism_turns = PRIAPISM_TURNS;
782069860236atagen2026-10-10 12:44 UTC334
        }
782069860236atagen2026-10-10 12:44 UTC335
        if self.conditions.priapism_turns > 0 {
782069860236atagen2026-10-10 12:44 UTC336
            self.conditions.priapism_turns -= 1;
782069860236atagen2026-10-10 12:44 UTC337
            self.eq = self.eq.max(PRIAPISM_EQ);
782069860236atagen2026-10-10 12:44 UTC338
        }
782069860236atagen2026-10-10 12:44 UTC339
        if self.turn == BRANCH_TURN && self.story == Story::Unopened {
782069860236atagen2026-10-10 12:44 UTC340
            self.story = Story::Pending;
782069860236atagen2026-10-10 12:44 UTC341
        }
782069860236atagen2026-10-10 12:44 UTC342
    }
782069860236atagen2026-10-10 12:44 UTC343
9683736a2c5eatagen2026-10-10 13:15 UTC344
    fn maybe_summon_mum(&mut self, was_below: bool) {
9683736a2c5eatagen2026-10-10 13:15 UTC345
        if !self.floor.has_mum() || self.mum.is_some() {
782069860236atagen2026-10-10 12:44 UTC346
            return;
782069860236atagen2026-10-10 12:44 UTC347
        }
782069860236atagen2026-10-10 12:44 UTC348
        let summoned = was_below && self.suspicion == SUSPICION_LIMIT;
782069860236atagen2026-10-10 12:44 UTC349
        let visits = u32::from(self.suspicion) + 1;
9683736a2c5eatagen2026-10-10 13:15 UTC350
        if summoned || self.rng.random_ratio(visits, super::balance::MUM_VISIT) {
782069860236atagen2026-10-10 12:44 UTC351
            let errand = self.errand(summoned);
782069860236atagen2026-10-10 12:44 UTC352
            let nonce = self.fresh_nonce();
9683736a2c5eatagen2026-10-10 13:15 UTC353
            self.mum = Some(MumVisit {
782069860236atagen2026-10-10 12:44 UTC354
                errand,
782069860236atagen2026-10-10 12:44 UTC355
                nonce,
782069860236atagen2026-10-10 12:44 UTC356
                strikes: 0,
782069860236atagen2026-10-10 12:44 UTC357
            });
782069860236atagen2026-10-10 12:44 UTC358
        }
782069860236atagen2026-10-10 12:44 UTC359
    }
782069860236atagen2026-10-10 12:44 UTC360
782069860236atagen2026-10-10 12:44 UTC361
    fn roll(
782069860236atagen2026-10-10 12:44 UTC362
        &mut self,
782069860236atagen2026-10-10 12:44 UTC363
        training: Training,
782069860236atagen2026-10-10 12:44 UTC364
        intensity: i32,
782069860236atagen2026-10-10 12:44 UTC365
        lube: Option<Lube>,
782069860236atagen2026-10-10 12:44 UTC366
    ) -> Result<Outcome, Problem> {
782069860236atagen2026-10-10 12:44 UTC367
        let profile = training.profile();
782069860236atagen2026-10-10 12:44 UTC368
        let natural: i32 = self.rng.random_range(1..=20);
782069860236atagen2026-10-10 12:44 UTC369
        let band = if EQ_BAND.contains(&self.eq) {
782069860236atagen2026-10-10 12:44 UTC370
            BAND_BONUS
782069860236atagen2026-10-10 12:44 UTC371
        } else {
782069860236atagen2026-10-10 12:44 UTC372
            0
782069860236atagen2026-10-10 12:44 UTC373
        };
782069860236atagen2026-10-10 12:44 UTC374
        let grip = match (self.grip, training) {
782069860236atagen2026-10-10 12:44 UTC375
            (Grip::Underhand, Training::Stretch) => 2,
782069860236atagen2026-10-10 12:44 UTC376
            (Grip::Underhand, Training::Wet) => -1,
782069860236atagen2026-10-10 12:44 UTC377
            _ => 0,
782069860236atagen2026-10-10 12:44 UTC378
        };
782069860236atagen2026-10-10 12:44 UTC379
        let modifier = lube.map_or(0, Lube::modifier) + band + grip;
782069860236atagen2026-10-10 12:44 UTC380
        let dc = profile.dc + intensity / 3;
782069860236atagen2026-10-10 12:44 UTC381
        let total = natural + modifier;
782069860236atagen2026-10-10 12:44 UTC382
        let timing = format!("roll;desc=\"d20{modifier:+} vs {dc}\";dur={natural}");
782069860236atagen2026-10-10 12:44 UTC383
        let timed = |p: Problem| p.header(SERVER_TIMING, &timing);
782069860236atagen2026-10-10 12:44 UTC384
782069860236atagen2026-10-10 12:44 UTC385
        let (length, girth) = self.scaled_gains(&profile, intensity);
782069860236atagen2026-10-10 12:44 UTC386
        let rupture_line = if self.items.ring {
782069860236atagen2026-10-10 12:44 UTC387
            RING_RUPTURE_LINE
782069860236atagen2026-10-10 12:44 UTC388
        } else {
782069860236atagen2026-10-10 12:44 UTC389
            RUPTURE_LINE
782069860236atagen2026-10-10 12:44 UTC390
        };
782069860236atagen2026-10-10 12:44 UTC391
782069860236atagen2026-10-10 12:44 UTC392
        if natural == 1 {
782069860236atagen2026-10-10 12:44 UTC393
            return Err(timed(self.critical_failure(training, &profile)));
782069860236atagen2026-10-10 12:44 UTC394
        }
782069860236atagen2026-10-10 12:44 UTC395
        if natural == 2 && self.eq > DANGER_EQ {
782069860236atagen2026-10-10 12:44 UTC396
            return Err(timed(self.rupture()));
782069860236atagen2026-10-10 12:44 UTC397
        }
782069860236atagen2026-10-10 12:44 UTC398
        if self.eq > rupture_line && total < dc {
782069860236atagen2026-10-10 12:44 UTC399
            return Err(timed(self.rupture()));
782069860236atagen2026-10-10 12:44 UTC400
        }
782069860236atagen2026-10-10 12:44 UTC401
        if natural == 20 {
782069860236atagen2026-10-10 12:44 UTC402
            self.gain(length * 2.0, girth * 2.0, profile.foreskin_growth);
782069860236atagen2026-10-10 12:44 UTC403
            return Ok(Outcome::json(
782069860236atagen2026-10-10 12:44 UTC404
                StatusCode::PERMANENT_REDIRECT,
782069860236atagen2026-10-10 12:44 UTC405
                json!({
782069860236atagen2026-10-10 12:44 UTC406
                    "detail": "Your length has moved permanently.",
782069860236atagen2026-10-10 12:44 UTC407
                    "length": round3(length * 2.0),
782069860236atagen2026-10-10 12:44 UTC408
                    "girth": round3(girth * 2.0),
782069860236atagen2026-10-10 12:44 UTC409
                }),
782069860236atagen2026-10-10 12:44 UTC410
            )
782069860236atagen2026-10-10 12:44 UTC411
            .header(header::LOCATION, self.path("/length"))
782069860236atagen2026-10-10 12:44 UTC412
            .header(SERVER_TIMING, timing));
782069860236atagen2026-10-10 12:44 UTC413
        }
782069860236atagen2026-10-10 12:44 UTC414
        if total < dc - 3 {
782069860236atagen2026-10-10 12:44 UTC415
            return Err(timed(not_extended()));
782069860236atagen2026-10-10 12:44 UTC416
        }
782069860236atagen2026-10-10 12:44 UTC417
        if total < dc {
782069860236atagen2026-10-10 12:44 UTC418
            return Ok(Outcome::empty(StatusCode::NO_CONTENT).header(SERVER_TIMING, timing));
782069860236atagen2026-10-10 12:44 UTC419
        }
782069860236atagen2026-10-10 12:44 UTC420
782069860236atagen2026-10-10 12:44 UTC421
        let outcome = if self.eq < *EQ_BAND.start() {
782069860236atagen2026-10-10 12:44 UTC422
            self.gain(length / 2.0, girth / 2.0, profile.foreskin_growth);
782069860236atagen2026-10-10 12:44 UTC423
            Outcome::json(
782069860236atagen2026-10-10 12:44 UTC424
                StatusCode::PARTIAL_CONTENT,
782069860236atagen2026-10-10 12:44 UTC425
                json!({
782069860236atagen2026-10-10 12:44 UTC426
                    "detail": "Partial gains were applied. EQ was below the band.",
782069860236atagen2026-10-10 12:44 UTC427
                    "length": round3(length / 2.0),
782069860236atagen2026-10-10 12:44 UTC428
                    "girth": round3(girth / 2.0),
782069860236atagen2026-10-10 12:44 UTC429
                }),
782069860236atagen2026-10-10 12:44 UTC430
            )
782069860236atagen2026-10-10 12:44 UTC431
        } else if total >= dc + 5 {
782069860236atagen2026-10-10 12:44 UTC432
            self.gain(length, girth, profile.foreskin_growth);
782069860236atagen2026-10-10 12:44 UTC433
            Outcome::json(
782069860236atagen2026-10-10 12:44 UTC434
                StatusCode::OK,
782069860236atagen2026-10-10 12:44 UTC435
                json!({
782069860236atagen2026-10-10 12:44 UTC436
                    "detail": "Gains were applied.",
782069860236atagen2026-10-10 12:44 UTC437
                    "length": round3(length),
782069860236atagen2026-10-10 12:44 UTC438
                    "girth": round3(girth),
782069860236atagen2026-10-10 12:44 UTC439
                }),
782069860236atagen2026-10-10 12:44 UTC440
            )
782069860236atagen2026-10-10 12:44 UTC441
        } else {
782069860236atagen2026-10-10 12:44 UTC442
            let eta = self.rng.random_range(1..=4);
782069860236atagen2026-10-10 12:44 UTC443
            self.pending.push(Pending {
782069860236atagen2026-10-10 12:44 UTC444
                due: self.turn + eta,
782069860236atagen2026-10-10 12:44 UTC445
                length,
782069860236atagen2026-10-10 12:44 UTC446
                girth,
782069860236atagen2026-10-10 12:44 UTC447
            });
782069860236atagen2026-10-10 12:44 UTC448
            self.gain(0.0, 0.0, profile.foreskin_growth);
782069860236atagen2026-10-10 12:44 UTC449
            Outcome::json(
782069860236atagen2026-10-10 12:44 UTC450
                StatusCode::ACCEPTED,
782069860236atagen2026-10-10 12:44 UTC451
                json!({
782069860236atagen2026-10-10 12:44 UTC452
                    "detail": "Gains have been queued. They may arrive.",
782069860236atagen2026-10-10 12:44 UTC453
                    "eta_turns": eta,
782069860236atagen2026-10-10 12:44 UTC454
                }),
782069860236atagen2026-10-10 12:44 UTC455
            )
782069860236atagen2026-10-10 12:44 UTC456
        };
782069860236atagen2026-10-10 12:44 UTC457
        Ok(outcome.header(SERVER_TIMING, timing))
782069860236atagen2026-10-10 12:44 UTC458
    }
782069860236atagen2026-10-10 12:44 UTC459
782069860236atagen2026-10-10 12:44 UTC460
    fn scaled_gains(&self, profile: &Profile, intensity: i32) -> (f64, f64) {
782069860236atagen2026-10-10 12:44 UTC461
        let scale = f64::from(intensity);
782069860236atagen2026-10-10 12:44 UTC462
        let (mut length, mut girth) = (profile.length * scale, profile.girth * scale);
782069860236atagen2026-10-10 12:44 UTC463
        if self.here.teapot {
782069860236atagen2026-10-10 12:44 UTC464
            length *= 0.5;
782069860236atagen2026-10-10 12:44 UTC465
            girth *= 1.5;
782069860236atagen2026-10-10 12:44 UTC466
        }
782069860236atagen2026-10-10 12:44 UTC467
        if self.items.ring {
782069860236atagen2026-10-10 12:44 UTC468
            girth *= 2.0;
782069860236atagen2026-10-10 12:44 UTC469
        }
782069860236atagen2026-10-10 12:44 UTC470
        (length, girth)
782069860236atagen2026-10-10 12:44 UTC471
    }
782069860236atagen2026-10-10 12:44 UTC472
782069860236atagen2026-10-10 12:44 UTC473
    fn critical_failure(&mut self, training: Training, profile: &Profile) -> Problem {
782069860236atagen2026-10-10 12:44 UTC474
        let uses_foreskin = profile.foreskin.is_some_and(|(min, _)| min > 0.0);
782069860236atagen2026-10-10 12:44 UTC475
        if uses_foreskin && self.foreskin.is_some() {
782069860236atagen2026-10-10 12:44 UTC476
            self.foreskin = None;
782069860236atagen2026-10-10 12:44 UTC477
            self.shame += 25;
782069860236atagen2026-10-10 12:44 UTC478
            return Problem::new(
782069860236atagen2026-10-10 12:44 UTC479
                StatusCode::GONE,
782069860236atagen2026-10-10 12:44 UTC480
                "foreskin-lost",
782069860236atagen2026-10-10 12:44 UTC481
                "The technique has removed the foreskin. It will not be returning.",
782069860236atagen2026-10-10 12:44 UTC482
            )
782069860236atagen2026-10-10 12:44 UTC483
            .header(header::CONTENT_LOCATION, self.path("/foreskin"));
782069860236atagen2026-10-10 12:44 UTC484
        }
782069860236atagen2026-10-10 12:44 UTC485
        if self.eq > DANGER_EQ {
782069860236atagen2026-10-10 12:44 UTC486
            return self.rupture();
782069860236atagen2026-10-10 12:44 UTC487
        }
782069860236atagen2026-10-10 12:44 UTC488
        if training == Training::Stretch && !self.here.teapot {
782069860236atagen2026-10-10 12:44 UTC489
            self.here.teapot = true;
782069860236atagen2026-10-10 12:44 UTC490
            self.items.teapot_spout = true;
782069860236atagen2026-10-10 12:44 UTC491
            self.shame += 5;
782069860236atagen2026-10-10 12:44 UTC492
            return Problem::new(
782069860236atagen2026-10-10 12:44 UTC493
                StatusCode::IM_A_TEAPOT,
782069860236atagen2026-10-10 12:44 UTC494
                "teapot",
782069860236atagen2026-10-10 12:44 UTC495
                "The subject is now short and stout. Length gains are halved for the rest of the floor.",
782069860236atagen2026-10-10 12:44 UTC496
            );
782069860236atagen2026-10-10 12:44 UTC497
        }
782069860236atagen2026-10-10 12:44 UTC498
        if training == Training::Uli && !self.conditions.peyronie {
782069860236atagen2026-10-10 12:44 UTC499
            self.conditions.peyronie = true;
782069860236atagen2026-10-10 12:44 UTC500
            self.shame += 5;
782069860236atagen2026-10-10 12:44 UTC501
            return Problem::new(
782069860236atagen2026-10-10 12:44 UTC502
                StatusCode::MISDIRECTED_REQUEST,
782069860236atagen2026-10-10 12:44 UTC503
                "curvature",
782069860236atagen2026-10-10 12:44 UTC504
                "A curvature has developed. Techniques will land slightly to the left until a professional is consulted.",
782069860236atagen2026-10-10 12:44 UTC505
            );
782069860236atagen2026-10-10 12:44 UTC506
        }
782069860236atagen2026-10-10 12:44 UTC507
        not_extended()
782069860236atagen2026-10-10 12:44 UTC508
    }
782069860236atagen2026-10-10 12:44 UTC509
782069860236atagen2026-10-10 12:44 UTC510
    fn gossip(&mut self, mut outcome: Outcome) -> Outcome {
782069860236atagen2026-10-10 12:44 UTC511
        let reportable = matches!(outcome.status.as_u16(), 200 | 202 | 206);
9683736a2c5eatagen2026-10-10 13:15 UTC512
        if !self.here.flatmate_told || !reportable {
782069860236atagen2026-10-10 12:44 UTC513
            return outcome;
782069860236atagen2026-10-10 12:44 UTC514
        }
9683736a2c5eatagen2026-10-10 13:15 UTC515
        self.here.flatmate_told = false;
782069860236atagen2026-10-10 12:44 UTC516
        outcome.status = StatusCode::ALREADY_REPORTED;
782069860236atagen2026-10-10 12:44 UTC517
        if let Payload::Json(body) = &mut outcome.payload {
9683736a2c5eatagen2026-10-10 13:15 UTC518
            body["detail"] = json!("The flatmate has already told everyone.");
782069860236atagen2026-10-10 12:44 UTC519
        }
782069860236atagen2026-10-10 12:44 UTC520
        outcome
782069860236atagen2026-10-10 12:44 UTC521
    }
782069860236atagen2026-10-10 12:44 UTC522
}