atagen

jelq

public

an http based roguelike rpg about jelqing

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

Blame: src/game/jelq.rs

dfa38cf4cd4batagen2026-10-10 09:17 UTC1
use std::{
dfa38cf4cd4batagen2026-10-10 09:17 UTC2
    fmt::Write as _,
dfa38cf4cd4batagen2026-10-10 09:17 UTC3
    time::{Duration, Instant, SystemTime},
dfa38cf4cd4batagen2026-10-10 09:17 UTC4
};
dfa38cf4cd4batagen2026-10-10 09:17 UTC5
dfa38cf4cd4batagen2026-10-10 09:17 UTC6
use axum::http::{StatusCode, header};
dfa38cf4cd4batagen2026-10-10 09:17 UTC7
use rand::Rng;
dfa38cf4cd4batagen2026-10-10 09:17 UTC8
use serde_json::json;
dfa38cf4cd4batagen2026-10-10 09:17 UTC9
dfa38cf4cd4batagen2026-10-10 09:17 UTC10
use super::{
dfa38cf4cd4batagen2026-10-10 09:17 UTC11
    CLINIC_ALLOW, FORUM_THREAD, Floor, Grip, Lube, Outcome, REFRACTORY, Run, SERVER_TIMING,
dfa38cf4cd4batagen2026-10-10 09:17 UTC12
    SUSPICION_LIMIT, Technique, catalog::LubeRule, catalog::Profile, not_extended, round3,
dfa38cf4cd4batagen2026-10-10 09:17 UTC13
};
dfa38cf4cd4batagen2026-10-10 09:17 UTC14
use crate::{game::LUBRICANTS, problem::Problem};
dfa38cf4cd4batagen2026-10-10 09:17 UTC15
dfa38cf4cd4batagen2026-10-10 09:17 UTC16
const WEEK: Duration = Duration::from_secs(7 * 24 * 60 * 60);
c3e0c27cf8fdatagen2026-10-10 11:28 UTC17
const PRIAPISM_TURNS: u8 = 5;
c3e0c27cf8fdatagen2026-10-10 11:28 UTC18
const PRIAPISM_EQ: i32 = 75;
c3e0c27cf8fdatagen2026-10-10 11:28 UTC19
const DEGLOVED: &str =
c3e0c27cf8fdatagen2026-10-10 11:28 UTC20
    "The outer layer has been removed in one piece. It will not be returning. The run has ended.";
dfa38cf4cd4batagen2026-10-10 09:17 UTC21
dfa38cf4cd4batagen2026-10-10 09:17 UTC22
impl Run {
dfa38cf4cd4batagen2026-10-10 09:17 UTC23
    pub fn jelq(
dfa38cf4cd4batagen2026-10-10 09:17 UTC24
        &mut self,
dfa38cf4cd4batagen2026-10-10 09:17 UTC25
        techniques: &[Technique],
dfa38cf4cd4batagen2026-10-10 09:17 UTC26
        intensity: i32,
dfa38cf4cd4batagen2026-10-10 09:17 UTC27
        lube: Option<Lube>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC28
        now: Instant,
dfa38cf4cd4batagen2026-10-10 09:17 UTC29
    ) -> Result<Outcome, Problem> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC30
        self.check_refractory(now)?;
dfa38cf4cd4batagen2026-10-10 09:17 UTC31
        if self.branch_pending {
dfa38cf4cd4batagen2026-10-10 09:17 UTC32
            return Ok(self.branch_offer());
dfa38cf4cd4batagen2026-10-10 09:17 UTC33
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC34
        match self.floor {
dfa38cf4cd4batagen2026-10-10 09:17 UTC35
            Floor::Forum => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC36
                return Ok(Outcome::json(
dfa38cf4cd4batagen2026-10-10 09:17 UTC37
                    StatusCode::SEE_OTHER,
dfa38cf4cd4batagen2026-10-10 09:17 UTC38
                    json!({ "detail": "Refer to the forum." }),
dfa38cf4cd4batagen2026-10-10 09:17 UTC39
                )
dfa38cf4cd4batagen2026-10-10 09:17 UTC40
                .header(header::LOCATION, FORUM_THREAD));
dfa38cf4cd4batagen2026-10-10 09:17 UTC41
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC42
            Floor::Clinic => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC43
                return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC44
                    StatusCode::METHOD_NOT_ALLOWED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC45
                    "not-in-the-clinic",
dfa38cf4cd4batagen2026-10-10 09:17 UTC46
                    "JELQ is not performed in the clinic.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC47
                )
dfa38cf4cd4batagen2026-10-10 09:17 UTC48
                .header(header::ALLOW, CLINIC_ALLOW));
dfa38cf4cd4batagen2026-10-10 09:17 UTC49
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC50
            _ => {}
dfa38cf4cd4batagen2026-10-10 09:17 UTC51
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC52
        if self.items.cage {
dfa38cf4cd4batagen2026-10-10 09:17 UTC53
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC54
                StatusCode::LOCKED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC55
                "caged",
dfa38cf4cd4batagen2026-10-10 09:17 UTC56
                "The cage is locked. The key is not in the box.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC57
            ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC58
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC59
        match *techniques {
dfa38cf4cd4batagen2026-10-10 09:17 UTC60
            [technique] => self.single(technique, intensity, lube, now),
dfa38cf4cd4batagen2026-10-10 09:17 UTC61
            [a, b] => self.combo(a, b, intensity, lube, now),
dfa38cf4cd4batagen2026-10-10 09:17 UTC62
            _ => Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC63
                StatusCode::BAD_REQUEST,
dfa38cf4cd4batagen2026-10-10 09:17 UTC64
                "too-many-techniques",
dfa38cf4cd4batagen2026-10-10 09:17 UTC65
                "Two techniques at most. The server has limits even if you do not.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC66
            )),
dfa38cf4cd4batagen2026-10-10 09:17 UTC67
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC68
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC69
dfa38cf4cd4batagen2026-10-10 09:17 UTC70
    fn single(
dfa38cf4cd4batagen2026-10-10 09:17 UTC71
        &mut self,
dfa38cf4cd4batagen2026-10-10 09:17 UTC72
        technique: Technique,
dfa38cf4cd4batagen2026-10-10 09:17 UTC73
        intensity: i32,
dfa38cf4cd4batagen2026-10-10 09:17 UTC74
        lube: Option<Lube>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC75
        now: Instant,
dfa38cf4cd4batagen2026-10-10 09:17 UTC76
    ) -> Result<Outcome, Problem> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC77
        match technique {
dfa38cf4cd4batagen2026-10-10 09:17 UTC78
            Technique::Warmup => return self.warm_up(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC79
            Technique::Ice => return self.ice(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC80
            Technique::Supplement => return self.supplement(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC81
            Technique::Edge => {}
dfa38cf4cd4batagen2026-10-10 09:17 UTC82
            _ => self.edges = 0,
dfa38cf4cd4batagen2026-10-10 09:17 UTC83
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC84
        self.prepare(technique, lube)?;
dfa38cf4cd4batagen2026-10-10 09:17 UTC85
        self.check_storage()?;
dfa38cf4cd4batagen2026-10-10 09:17 UTC86
        self.floor_enemy()?;
dfa38cf4cd4batagen2026-10-10 09:17 UTC87
        match technique {
dfa38cf4cd4batagen2026-10-10 09:17 UTC88
            Technique::Edge => return self.edge(now),
dfa38cf4cd4batagen2026-10-10 09:17 UTC89
            Technique::Pump => return self.pump(now),
dfa38cf4cd4batagen2026-10-10 09:17 UTC90
            _ => {}
dfa38cf4cd4batagen2026-10-10 09:17 UTC91
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC92
        let profile = technique.profile().expect("training technique");
dfa38cf4cd4batagen2026-10-10 09:17 UTC93
        self.commit(intensity, lube, now, 1);
c3e0c27cf8fdatagen2026-10-10 11:28 UTC94
        if technique == Technique::Dry && intensity >= 8 && self.rng.random_ratio(1, 60) {
c3e0c27cf8fdatagen2026-10-10 11:28 UTC95
            self.end("degloved", DEGLOVED);
c3e0c27cf8fdatagen2026-10-10 11:28 UTC96
            return Err(Problem::new(StatusCode::GONE, "degloved", DEGLOVED));
c3e0c27cf8fdatagen2026-10-10 11:28 UTC97
        }
c3e0c27cf8fdatagen2026-10-10 11:28 UTC98
        if let Some(interrupted) = self.hazards(intensity)? {
c3e0c27cf8fdatagen2026-10-10 11:28 UTC99
            return Ok(interrupted);
c3e0c27cf8fdatagen2026-10-10 11:28 UTC100
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC101
        let result = self.roll(technique, &profile, intensity, lube);
dfa38cf4cd4batagen2026-10-10 09:17 UTC102
        Ok(self.gossip(result?))
dfa38cf4cd4batagen2026-10-10 09:17 UTC103
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC104
dfa38cf4cd4batagen2026-10-10 09:17 UTC105
    fn combo(
dfa38cf4cd4batagen2026-10-10 09:17 UTC106
        &mut self,
dfa38cf4cd4batagen2026-10-10 09:17 UTC107
        a: Technique,
dfa38cf4cd4batagen2026-10-10 09:17 UTC108
        b: Technique,
dfa38cf4cd4batagen2026-10-10 09:17 UTC109
        intensity: i32,
dfa38cf4cd4batagen2026-10-10 09:17 UTC110
        lube: Option<Lube>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC111
        now: Instant,
dfa38cf4cd4batagen2026-10-10 09:17 UTC112
    ) -> Result<Outcome, Problem> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC113
        let (Some(first), Some(second)) = (a.profile(), b.profile()) else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC114
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC115
                StatusCode::BAD_REQUEST,
dfa38cf4cd4batagen2026-10-10 09:17 UTC116
                "combo-invalid",
dfa38cf4cd4batagen2026-10-10 09:17 UTC117
                "Only training techniques can be combined.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC118
            ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC119
        };
dfa38cf4cd4batagen2026-10-10 09:17 UTC120
        if a == b {
dfa38cf4cd4batagen2026-10-10 09:17 UTC121
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC122
                StatusCode::BAD_REQUEST,
dfa38cf4cd4batagen2026-10-10 09:17 UTC123
                "combo-invalid",
dfa38cf4cd4batagen2026-10-10 09:17 UTC124
                "A technique cannot be combined with itself. The server has checked.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC125
            ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC126
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC127
        self.edges = 0;
dfa38cf4cd4batagen2026-10-10 09:17 UTC128
        self.prepare(a, lube)?;
dfa38cf4cd4batagen2026-10-10 09:17 UTC129
        self.prepare(b, lube)?;
dfa38cf4cd4batagen2026-10-10 09:17 UTC130
        self.check_storage()?;
dfa38cf4cd4batagen2026-10-10 09:17 UTC131
        self.floor_enemy()?;
dfa38cf4cd4batagen2026-10-10 09:17 UTC132
        self.commit(intensity, lube, now, 2);
c3e0c27cf8fdatagen2026-10-10 11:28 UTC133
        if let Some(interrupted) = self.hazards(intensity)? {
c3e0c27cf8fdatagen2026-10-10 11:28 UTC134
            return Ok(interrupted);
c3e0c27cf8fdatagen2026-10-10 11:28 UTC135
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC136
dfa38cf4cd4batagen2026-10-10 09:17 UTC137
        let mut statuses = Vec::new();
dfa38cf4cd4batagen2026-10-10 09:17 UTC138
        for (technique, profile) in [(a, first), (b, second)] {
dfa38cf4cd4batagen2026-10-10 09:17 UTC139
            let result = self.roll(technique, &profile, intensity, lube);
dfa38cf4cd4batagen2026-10-10 09:17 UTC140
            if self.ended.is_some() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC141
                return result;
dfa38cf4cd4batagen2026-10-10 09:17 UTC142
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC143
            let status = match &result {
dfa38cf4cd4batagen2026-10-10 09:17 UTC144
                Ok(outcome) => outcome.status(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC145
                Err(problem) => problem.status(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC146
            };
dfa38cf4cd4batagen2026-10-10 09:17 UTC147
            statuses.push((technique, status));
dfa38cf4cd4batagen2026-10-10 09:17 UTC148
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC149
dfa38cf4cd4batagen2026-10-10 09:17 UTC150
        let mut xml = String::from(
dfa38cf4cd4batagen2026-10-10 09:17 UTC151
            "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<D:multistatus xmlns:D=\"DAV:\">\n",
dfa38cf4cd4batagen2026-10-10 09:17 UTC152
        );
dfa38cf4cd4batagen2026-10-10 09:17 UTC153
        for (technique, status) in statuses {
dfa38cf4cd4batagen2026-10-10 09:17 UTC154
            let _ = write!(
dfa38cf4cd4batagen2026-10-10 09:17 UTC155
                xml,
dfa38cf4cd4batagen2026-10-10 09:17 UTC156
                "  <D:response>\n    <D:href>/runs/{}?technique={}</D:href>\n    <D:status>HTTP/1.1 {} {}</D:status>\n  </D:response>\n",
dfa38cf4cd4batagen2026-10-10 09:17 UTC157
                self.id,
dfa38cf4cd4batagen2026-10-10 09:17 UTC158
                technique.name(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC159
                status.as_u16(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC160
                status.canonical_reason().unwrap_or_default(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC161
            );
dfa38cf4cd4batagen2026-10-10 09:17 UTC162
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC163
        xml.push_str(
dfa38cf4cd4batagen2026-10-10 09:17 UTC164
            "  <D:responsedescription>Each technique has been reported separately.</D:responsedescription>\n</D:multistatus>\n",
dfa38cf4cd4batagen2026-10-10 09:17 UTC165
        );
dfa38cf4cd4batagen2026-10-10 09:17 UTC166
        Ok(Outcome::raw(
dfa38cf4cd4batagen2026-10-10 09:17 UTC167
            StatusCode::MULTI_STATUS,
dfa38cf4cd4batagen2026-10-10 09:17 UTC168
            "application/xml; charset=utf-8",
dfa38cf4cd4batagen2026-10-10 09:17 UTC169
            xml,
dfa38cf4cd4batagen2026-10-10 09:17 UTC170
        ))
dfa38cf4cd4batagen2026-10-10 09:17 UTC171
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC172
dfa38cf4cd4batagen2026-10-10 09:17 UTC173
    fn prepare(&self, technique: Technique, lube: Option<Lube>) -> Result<(), Problem> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC174
        if technique == Technique::Uli && !self.forum_read {
dfa38cf4cd4batagen2026-10-10 09:17 UTC175
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC176
                StatusCode::FORBIDDEN,
dfa38cf4cd4batagen2026-10-10 09:17 UTC177
                "technique-locked",
dfa38cf4cd4batagen2026-10-10 09:17 UTC178
                "This technique is discussed on the forum. You have not been to the forum.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC179
            ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC180
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC181
        if !self.warmed_up {
dfa38cf4cd4batagen2026-10-10 09:17 UTC182
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC183
                StatusCode::PRECONDITION_FAILED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC184
                "cold-start",
dfa38cf4cd4batagen2026-10-10 09:17 UTC185
                "Warm-up was skipped. The server has noted this.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC186
            )
dfa38cf4cd4batagen2026-10-10 09:17 UTC187
            .with("technique", Technique::Warmup.name()));
dfa38cf4cd4batagen2026-10-10 09:17 UTC188
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC189
        if technique == Technique::Pump && !self.items.pump_pro {
dfa38cf4cd4batagen2026-10-10 09:17 UTC190
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC191
                StatusCode::FAILED_DEPENDENCY,
dfa38cf4cd4batagen2026-10-10 09:17 UTC192
                "no-pump",
dfa38cf4cd4batagen2026-10-10 09:17 UTC193
                "The pump technique depends on a pump.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC194
            ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC195
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC196
        let Some(profile) = technique.profile() else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC197
            return Ok(());
dfa38cf4cd4batagen2026-10-10 09:17 UTC198
        };
dfa38cf4cd4batagen2026-10-10 09:17 UTC199
        match (profile.lube, lube) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC200
            (LubeRule::Required, None) => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC201
                return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC202
                    StatusCode::UNSUPPORTED_MEDIA_TYPE,
dfa38cf4cd4batagen2026-10-10 09:17 UTC203
                    "lubricant-required",
dfa38cf4cd4batagen2026-10-10 09:17 UTC204
                    format!(
dfa38cf4cd4batagen2026-10-10 09:17 UTC205
                        "The {} technique requires a lubricant. None was declared.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC206
                        technique.name()
dfa38cf4cd4batagen2026-10-10 09:17 UTC207
                    ),
dfa38cf4cd4batagen2026-10-10 09:17 UTC208
                )
dfa38cf4cd4batagen2026-10-10 09:17 UTC209
                .with("lubricants", json!(LUBRICANTS)));
dfa38cf4cd4batagen2026-10-10 09:17 UTC210
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC211
            (LubeRule::Forbidden, Some(_)) => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC212
                return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC213
                    StatusCode::UNSUPPORTED_MEDIA_TYPE,
dfa38cf4cd4batagen2026-10-10 09:17 UTC214
                    "lubricant-declared",
dfa38cf4cd4batagen2026-10-10 09:17 UTC215
                    "The dry technique was requested. A lubricant was declared.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC216
                ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC217
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC218
            _ => {}
dfa38cf4cd4batagen2026-10-10 09:17 UTC219
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC220
        if let Some((min, max)) = profile.foreskin {
dfa38cf4cd4batagen2026-10-10 09:17 UTC221
            self.check_foreskin(min, max)?;
dfa38cf4cd4batagen2026-10-10 09:17 UTC222
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC223
        Ok(())
dfa38cf4cd4batagen2026-10-10 09:17 UTC224
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC225
dfa38cf4cd4batagen2026-10-10 09:17 UTC226
    fn check_foreskin(&self, min: f64, max: f64) -> Result<(), Problem> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC227
        let Some(f) = self.foreskin else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC228
            if min <= 0.0 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC229
                return Ok(());
dfa38cf4cd4batagen2026-10-10 09:17 UTC230
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC231
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC232
                StatusCode::GONE,
dfa38cf4cd4batagen2026-10-10 09:17 UTC233
                "foreskin-gone",
dfa38cf4cd4batagen2026-10-10 09:17 UTC234
                "This technique requires a foreskin. The foreskin is gone.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC235
            )
dfa38cf4cd4batagen2026-10-10 09:17 UTC236
            .header(
dfa38cf4cd4batagen2026-10-10 09:17 UTC237
                header::CONTENT_LOCATION,
dfa38cf4cd4batagen2026-10-10 09:17 UTC238
                format!("/runs/{}/foreskin", self.id),
dfa38cf4cd4batagen2026-10-10 09:17 UTC239
            ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC240
        };
dfa38cf4cd4batagen2026-10-10 09:17 UTC241
        if f < min {
dfa38cf4cd4batagen2026-10-10 09:17 UTC242
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC243
                StatusCode::LENGTH_REQUIRED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC244
                "foreskin-insufficient",
dfa38cf4cd4batagen2026-10-10 09:17 UTC245
                format!(
dfa38cf4cd4batagen2026-10-10 09:17 UTC246
                    "The selected technique requires {min:.1} units of foreskin. {f:.2} units are available. The server has noted the shortfall."
dfa38cf4cd4batagen2026-10-10 09:17 UTC247
                ),
dfa38cf4cd4batagen2026-10-10 09:17 UTC248
            )
dfa38cf4cd4batagen2026-10-10 09:17 UTC249
            .with("required", min)
dfa38cf4cd4batagen2026-10-10 09:17 UTC250
            .with("available", super::round2(f)));
dfa38cf4cd4batagen2026-10-10 09:17 UTC251
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC252
        if f > max {
dfa38cf4cd4batagen2026-10-10 09:17 UTC253
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC254
                StatusCode::PAYLOAD_TOO_LARGE,
dfa38cf4cd4batagen2026-10-10 09:17 UTC255
                "foreskin-excessive",
dfa38cf4cd4batagen2026-10-10 09:17 UTC256
                format!(
dfa38cf4cd4batagen2026-10-10 09:17 UTC257
                    "The selected technique tolerates at most {max:.1} units of foreskin. {f:.2} units are present."
dfa38cf4cd4batagen2026-10-10 09:17 UTC258
                ),
dfa38cf4cd4batagen2026-10-10 09:17 UTC259
            )
dfa38cf4cd4batagen2026-10-10 09:17 UTC260
            .with("maximum", max)
dfa38cf4cd4batagen2026-10-10 09:17 UTC261
            .with("present", super::round2(f)));
dfa38cf4cd4batagen2026-10-10 09:17 UTC262
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC263
        Ok(())
dfa38cf4cd4batagen2026-10-10 09:17 UTC264
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC265
dfa38cf4cd4batagen2026-10-10 09:17 UTC266
    fn check_storage(&self) -> Result<(), Problem> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC267
        if self.items.extender && self.girth > 5.0 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC268
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC269
                StatusCode::INSUFFICIENT_STORAGE,
dfa38cf4cd4batagen2026-10-10 09:17 UTC270
                "jeans-full",
dfa38cf4cd4batagen2026-10-10 09:17 UTC271
                "The jeans are full. The extender is occupying the remaining storage.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC272
            )
dfa38cf4cd4batagen2026-10-10 09:17 UTC273
            .with("extender", format!("/runs/{}/inventory/extender", self.id)));
dfa38cf4cd4batagen2026-10-10 09:17 UTC274
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC275
        Ok(())
dfa38cf4cd4batagen2026-10-10 09:17 UTC276
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC277
dfa38cf4cd4batagen2026-10-10 09:17 UTC278
    fn commit(&mut self, intensity: i32, lube: Option<Lube>, now: Instant, cost: i32) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC279
        self.touch();
dfa38cf4cd4batagen2026-10-10 09:17 UTC280
        self.turn += 1;
dfa38cf4cd4batagen2026-10-10 09:17 UTC281
        self.floor_turns += 1;
dfa38cf4cd4batagen2026-10-10 09:17 UTC282
        self.realize();
dfa38cf4cd4batagen2026-10-10 09:17 UTC283
        if self.items.extender {
dfa38cf4cd4batagen2026-10-10 09:17 UTC284
            self.length += 0.004;
dfa38cf4cd4batagen2026-10-10 09:17 UTC285
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC286
        self.stamina -= cost + i32::from(lube == Some(Lube::Lotion));
dfa38cf4cd4batagen2026-10-10 09:17 UTC287
        if self.stamina <= 0 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC288
            self.stamina = 0;
99524cf9f17catagen2026-10-10 10:53 UTC289
            self.refractory_until = Some(now + *REFRACTORY);
dfa38cf4cd4batagen2026-10-10 09:17 UTC290
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC291
        let chill = if self.floor == Floor::LockerRoom {
dfa38cf4cd4batagen2026-10-10 09:17 UTC292
            8
dfa38cf4cd4batagen2026-10-10 09:17 UTC293
        } else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC294
            0
dfa38cf4cd4batagen2026-10-10 09:17 UTC295
        };
99524cf9f17catagen2026-10-10 10:53 UTC296
        let wobble = self.rng.random_range(-3..=3);
99524cf9f17catagen2026-10-10 10:53 UTC297
        self.eq = (self.eq + intensity * 2 - 6 - chill + wobble).clamp(0, 100);
dfa38cf4cd4batagen2026-10-10 09:17 UTC298
dfa38cf4cd4batagen2026-10-10 09:17 UTC299
        let mut suspicion = intensity / 5 + i32::from(lube == Some(Lube::Vaseline));
dfa38cf4cd4batagen2026-10-10 09:17 UTC300
        if matches!(self.floor, Floor::LockerRoom | Floor::Grandmas) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC301
            suspicion *= 2;
dfa38cf4cd4batagen2026-10-10 09:17 UTC302
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC303
        if self.floor == Floor::Grandmas && lube == Some(Lube::CoconutOil) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC304
            suspicion += 2;
dfa38cf4cd4batagen2026-10-10 09:17 UTC305
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC306
        let was_below = self.suspicion < SUSPICION_LIMIT;
dfa38cf4cd4batagen2026-10-10 09:17 UTC307
        self.suspicion =
dfa38cf4cd4batagen2026-10-10 09:17 UTC308
            (i32::from(self.suspicion) + suspicion).min(i32::from(SUSPICION_LIMIT)) as u8;
dfa38cf4cd4batagen2026-10-10 09:17 UTC309
        if self.floor.has_mom() && !self.mom_present {
dfa38cf4cd4batagen2026-10-10 09:17 UTC310
            let summoned = was_below && self.suspicion == SUSPICION_LIMIT;
dfa38cf4cd4batagen2026-10-10 09:17 UTC311
            if summoned || self.rng.random_ratio(u32::from(self.suspicion) + 1, 16) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC312
                self.mom_present = true;
99524cf9f17catagen2026-10-10 10:53 UTC313
                self.mom_strikes = 0;
dfa38cf4cd4batagen2026-10-10 09:17 UTC314
                self.mom_errand = self.errand(summoned);
dfa38cf4cd4batagen2026-10-10 09:17 UTC315
                self.nonce = self.fresh_nonce();
dfa38cf4cd4batagen2026-10-10 09:17 UTC316
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC317
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC318
dfa38cf4cd4batagen2026-10-10 09:17 UTC319
        if let Some(observer) = self.floor.observer()
dfa38cf4cd4batagen2026-10-10 09:17 UTC320
            && self.rng.random_ratio(1, 6)
dfa38cf4cd4batagen2026-10-10 09:17 UTC321
        {
dfa38cf4cd4batagen2026-10-10 09:17 UTC322
            self.observed = Some(observer);
dfa38cf4cd4batagen2026-10-10 09:17 UTC323
            self.shame += 1;
c3e0c27cf8fdatagen2026-10-10 11:28 UTC324
            if observer == "cat" && self.rng.random_ratio(1, 5) {
c3e0c27cf8fdatagen2026-10-10 11:28 UTC325
                self.towel_stolen = true;
c3e0c27cf8fdatagen2026-10-10 11:28 UTC326
            }
c3e0c27cf8fdatagen2026-10-10 11:28 UTC327
        }
c3e0c27cf8fdatagen2026-10-10 11:28 UTC328
        if intensity >= 9 && !self.pulled_muscle && self.rng.random_ratio(1, 12) {
c3e0c27cf8fdatagen2026-10-10 11:28 UTC329
            self.pulled_muscle = true;
c3e0c27cf8fdatagen2026-10-10 11:28 UTC330
            self.stamina = self.stamina.min(self.max_stamina());
c3e0c27cf8fdatagen2026-10-10 11:28 UTC331
        }
c3e0c27cf8fdatagen2026-10-10 11:28 UTC332
        if self.items.ring && self.eq > 85 && self.rng.random_ratio(1, 10) {
c3e0c27cf8fdatagen2026-10-10 11:28 UTC333
            self.priapism_turns = PRIAPISM_TURNS;
c3e0c27cf8fdatagen2026-10-10 11:28 UTC334
        }
c3e0c27cf8fdatagen2026-10-10 11:28 UTC335
        if self.priapism_turns > 0 {
c3e0c27cf8fdatagen2026-10-10 11:28 UTC336
            self.priapism_turns -= 1;
c3e0c27cf8fdatagen2026-10-10 11:28 UTC337
            self.eq = self.eq.max(PRIAPISM_EQ);
dfa38cf4cd4batagen2026-10-10 09:17 UTC338
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC339
        if self.turn == super::BRANCH_TURN {
dfa38cf4cd4batagen2026-10-10 09:17 UTC340
            self.branch_pending = true;
dfa38cf4cd4batagen2026-10-10 09:17 UTC341
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC342
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC343
c3e0c27cf8fdatagen2026-10-10 11:28 UTC344
    fn hazards(&mut self, intensity: i32) -> Result<Option<Outcome>, Problem> {
2ea5ef81f040atagen2026-10-10 11:24 UTC345
        let odds = match self.eq {
d1cbd183925datagen2026-10-10 11:58 UTC346
            ..=70 => 250,
2ea5ef81f040atagen2026-10-10 11:24 UTC347
            71..=85 => 25,
2ea5ef81f040atagen2026-10-10 11:24 UTC348
            _ => 10,
2ea5ef81f040atagen2026-10-10 11:24 UTC349
        };
2ea5ef81f040atagen2026-10-10 11:24 UTC350
        if self.rng.random_ratio(1, odds) {
2ea5ef81f040atagen2026-10-10 11:24 UTC351
            return Err(self.rupture());
2ea5ef81f040atagen2026-10-10 11:24 UTC352
        }
c3e0c27cf8fdatagen2026-10-10 11:28 UTC353
        let windowed = matches!(self.floor, Floor::Bedroom | Floor::Grandmas | Floor::Hotel);
c3e0c27cf8fdatagen2026-10-10 11:28 UTC354
        if windowed && !self.items.blinds && self.rng.random_ratio(1, 60) {
c3e0c27cf8fdatagen2026-10-10 11:28 UTC355
            self.shame += 30;
c3e0c27cf8fdatagen2026-10-10 11:28 UTC356
            self.filmed_at.get_or_insert_with(SystemTime::now);
c3e0c27cf8fdatagen2026-10-10 11:28 UTC357
            return Ok(Some(
c3e0c27cf8fdatagen2026-10-10 11:28 UTC358
                Outcome::json(
c3e0c27cf8fdatagen2026-10-10 11:28 UTC359
                    StatusCode::CREATED,
c3e0c27cf8fdatagen2026-10-10 11:28 UTC360
                    json!({ "detail": "A neighbour was at the window. A recording has been created. It has been shared." }),
c3e0c27cf8fdatagen2026-10-10 11:28 UTC361
                )
c3e0c27cf8fdatagen2026-10-10 11:28 UTC362
                .header(header::LOCATION, format!("/runs/{}/footage", self.id)),
c3e0c27cf8fdatagen2026-10-10 11:28 UTC363
            ));
c3e0c27cf8fdatagen2026-10-10 11:28 UTC364
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC365
        if self.floor == Floor::LockerRoom && self.rng.random_ratio(1, 10) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC366
            self.shame += 1;
dfa38cf4cd4batagen2026-10-10 09:17 UTC367
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC368
                StatusCode::USE_PROXY,
dfa38cf4cd4batagen2026-10-10 09:17 UTC369
                "friend",
dfa38cf4cd4batagen2026-10-10 09:17 UTC370
                "A friend has offered to help. Use Proxy is deprecated, and the server has declined on your behalf.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC371
            ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC372
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC373
        if self.peyronie && self.rng.random_ratio(1, 3) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC374
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC375
                StatusCode::MISDIRECTED_REQUEST,
dfa38cf4cd4batagen2026-10-10 09:17 UTC376
                "misdirected",
dfa38cf4cd4batagen2026-10-10 09:17 UTC377
                "The technique landed slightly to the left. It was meant for a different server.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC378
            ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC379
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC380
        if intensity <= 2 && self.eq < 50 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC381
            self.eq = (self.eq - 10).max(0);
dfa38cf4cd4batagen2026-10-10 09:17 UTC382
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC383
                StatusCode::REQUEST_TIMEOUT,
dfa38cf4cd4batagen2026-10-10 09:17 UTC384
                "lost-it",
dfa38cf4cd4batagen2026-10-10 09:17 UTC385
                "The request took too long. It was lost.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC386
            )
dfa38cf4cd4batagen2026-10-10 09:17 UTC387
            .header(header::CONNECTION, "close"));
dfa38cf4cd4batagen2026-10-10 09:17 UTC388
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC389
        if self.eq > 95 {
177535ac8abfatagen2026-10-10 10:39 UTC390
            if self.rng.random_ratio(1, 3) {
177535ac8abfatagen2026-10-10 10:39 UTC391
                return Err(self.rupture());
177535ac8abfatagen2026-10-10 10:39 UTC392
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC393
            self.shame += 5;
99524cf9f17catagen2026-10-10 10:53 UTC394
            self.next_floor(false);
dfa38cf4cd4batagen2026-10-10 09:17 UTC395
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC396
                StatusCode::TOO_EARLY,
dfa38cf4cd4batagen2026-10-10 09:17 UTC397
                "premature",
dfa38cf4cd4batagen2026-10-10 09:17 UTC398
                "The floor has ended early. Everyone involved has agreed not to discuss it.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC399
            )
dfa38cf4cd4batagen2026-10-10 09:17 UTC400
            .with("floor", self.floor.number()));
dfa38cf4cd4batagen2026-10-10 09:17 UTC401
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC402
        if self.eq < 30 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC403
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC404
                StatusCode::NOT_FOUND,
dfa38cf4cd4batagen2026-10-10 09:17 UTC405
                "cold",
dfa38cf4cd4batagen2026-10-10 09:17 UTC406
                "The resource exists. It is cold.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC407
            )
dfa38cf4cd4batagen2026-10-10 09:17 UTC408
            .with("eq", self.eq));
dfa38cf4cd4batagen2026-10-10 09:17 UTC409
        }
c3e0c27cf8fdatagen2026-10-10 11:28 UTC410
        Ok(None)
dfa38cf4cd4batagen2026-10-10 09:17 UTC411
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC412
dfa38cf4cd4batagen2026-10-10 09:17 UTC413
    fn roll(
dfa38cf4cd4batagen2026-10-10 09:17 UTC414
        &mut self,
dfa38cf4cd4batagen2026-10-10 09:17 UTC415
        technique: Technique,
dfa38cf4cd4batagen2026-10-10 09:17 UTC416
        profile: &Profile,
dfa38cf4cd4batagen2026-10-10 09:17 UTC417
        intensity: i32,
dfa38cf4cd4batagen2026-10-10 09:17 UTC418
        lube: Option<Lube>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC419
    ) -> Result<Outcome, Problem> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC420
        let natural: i32 = self.rng.random_range(1..=20);
dfa38cf4cd4batagen2026-10-10 09:17 UTC421
        let band = if (50..=70).contains(&self.eq) { 2 } else { 0 };
dfa38cf4cd4batagen2026-10-10 09:17 UTC422
        let grip = match (self.grip, technique) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC423
            (Grip::Underhand, Technique::Stretch) => 2,
dfa38cf4cd4batagen2026-10-10 09:17 UTC424
            (Grip::Underhand, Technique::Wet) => -1,
dfa38cf4cd4batagen2026-10-10 09:17 UTC425
            _ => 0,
dfa38cf4cd4batagen2026-10-10 09:17 UTC426
        };
dfa38cf4cd4batagen2026-10-10 09:17 UTC427
        let modifier = lube.map_or(0, Lube::modifier) + band + grip;
dfa38cf4cd4batagen2026-10-10 09:17 UTC428
        let dc = profile.dc + intensity / 3;
dfa38cf4cd4batagen2026-10-10 09:17 UTC429
        let total = natural + modifier;
dfa38cf4cd4batagen2026-10-10 09:17 UTC430
        let timing = format!("roll;desc=\"d20{modifier:+} vs {dc}\";dur={natural}");
dfa38cf4cd4batagen2026-10-10 09:17 UTC431
        let timed = |p: Problem| p.header(SERVER_TIMING, &timing);
dfa38cf4cd4batagen2026-10-10 09:17 UTC432
dfa38cf4cd4batagen2026-10-10 09:17 UTC433
        let scale = f64::from(intensity);
dfa38cf4cd4batagen2026-10-10 09:17 UTC434
        let (mut length, mut girth) = (profile.length * scale, profile.girth * scale);
dfa38cf4cd4batagen2026-10-10 09:17 UTC435
        if self.teapot {
dfa38cf4cd4batagen2026-10-10 09:17 UTC436
            length *= 0.5;
dfa38cf4cd4batagen2026-10-10 09:17 UTC437
            girth *= 1.5;
dfa38cf4cd4batagen2026-10-10 09:17 UTC438
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC439
        if self.items.ring {
dfa38cf4cd4batagen2026-10-10 09:17 UTC440
            girth *= 2.0;
dfa38cf4cd4batagen2026-10-10 09:17 UTC441
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC442
        let rupture_eq = if self.items.ring { 80 } else { 90 };
dfa38cf4cd4batagen2026-10-10 09:17 UTC443
dfa38cf4cd4batagen2026-10-10 09:17 UTC444
        if natural == 1 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC445
            let touches_foreskin = profile.foreskin.is_some_and(|(min, _)| min > 0.0);
dfa38cf4cd4batagen2026-10-10 09:17 UTC446
            if touches_foreskin && self.foreskin.is_some() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC447
                self.foreskin = None;
dfa38cf4cd4batagen2026-10-10 09:17 UTC448
                self.shame += 25;
dfa38cf4cd4batagen2026-10-10 09:17 UTC449
                return Err(timed(
dfa38cf4cd4batagen2026-10-10 09:17 UTC450
                    Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC451
                        StatusCode::GONE,
dfa38cf4cd4batagen2026-10-10 09:17 UTC452
                        "foreskin-lost",
dfa38cf4cd4batagen2026-10-10 09:17 UTC453
                        "The technique has removed the foreskin. It will not be returning.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC454
                    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC455
                    .header(
dfa38cf4cd4batagen2026-10-10 09:17 UTC456
                        header::CONTENT_LOCATION,
dfa38cf4cd4batagen2026-10-10 09:17 UTC457
                        format!("/runs/{}/foreskin", self.id),
dfa38cf4cd4batagen2026-10-10 09:17 UTC458
                    ),
dfa38cf4cd4batagen2026-10-10 09:17 UTC459
                ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC460
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC461
            if self.eq > 70 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC462
                return Err(timed(self.rupture()));
dfa38cf4cd4batagen2026-10-10 09:17 UTC463
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC464
            if technique == Technique::Stretch && !self.teapot {
dfa38cf4cd4batagen2026-10-10 09:17 UTC465
                self.teapot = true;
dfa38cf4cd4batagen2026-10-10 09:17 UTC466
                self.items.teapot_spout = true;
dfa38cf4cd4batagen2026-10-10 09:17 UTC467
                self.shame += 5;
dfa38cf4cd4batagen2026-10-10 09:17 UTC468
                return Err(timed(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC469
                    StatusCode::IM_A_TEAPOT,
dfa38cf4cd4batagen2026-10-10 09:17 UTC470
                    "teapot",
dfa38cf4cd4batagen2026-10-10 09:17 UTC471
                    "The subject is now short and stout. Length gains are halved for the rest of the floor.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC472
                )));
dfa38cf4cd4batagen2026-10-10 09:17 UTC473
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC474
            if technique == Technique::Uli && !self.peyronie {
dfa38cf4cd4batagen2026-10-10 09:17 UTC475
                self.peyronie = true;
dfa38cf4cd4batagen2026-10-10 09:17 UTC476
                self.shame += 5;
dfa38cf4cd4batagen2026-10-10 09:17 UTC477
                return Err(timed(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC478
                    StatusCode::MISDIRECTED_REQUEST,
dfa38cf4cd4batagen2026-10-10 09:17 UTC479
                    "curvature",
dfa38cf4cd4batagen2026-10-10 09:17 UTC480
                    "A curvature has developed. Techniques will land slightly to the left until a professional is consulted.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC481
                )));
dfa38cf4cd4batagen2026-10-10 09:17 UTC482
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC483
            return Err(timed(not_extended()));
dfa38cf4cd4batagen2026-10-10 09:17 UTC484
        }
99524cf9f17catagen2026-10-10 10:53 UTC485
        if natural == 2 && self.eq > 70 {
99524cf9f17catagen2026-10-10 10:53 UTC486
            return Err(timed(self.rupture()));
99524cf9f17catagen2026-10-10 10:53 UTC487
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC488
        if self.eq > rupture_eq && total < dc {
dfa38cf4cd4batagen2026-10-10 09:17 UTC489
            return Err(timed(self.rupture()));
dfa38cf4cd4batagen2026-10-10 09:17 UTC490
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC491
        if natural == 20 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC492
            self.gain(length * 2.0, girth * 2.0, profile.foreskin_growth);
dfa38cf4cd4batagen2026-10-10 09:17 UTC493
            return Ok(Outcome::json(
dfa38cf4cd4batagen2026-10-10 09:17 UTC494
                StatusCode::CREATED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC495
                json!({
dfa38cf4cd4batagen2026-10-10 09:17 UTC496
                    "detail": "A permanent gain has been created.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC497
                    "length": round3(length * 2.0),
dfa38cf4cd4batagen2026-10-10 09:17 UTC498
                    "girth": round3(girth * 2.0),
dfa38cf4cd4batagen2026-10-10 09:17 UTC499
                }),
dfa38cf4cd4batagen2026-10-10 09:17 UTC500
            )
dfa38cf4cd4batagen2026-10-10 09:17 UTC501
            .header(header::LOCATION, format!("/runs/{}/length", self.id))
dfa38cf4cd4batagen2026-10-10 09:17 UTC502
            .header(SERVER_TIMING, timing));
dfa38cf4cd4batagen2026-10-10 09:17 UTC503
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC504
        if total < dc - 3 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC505
            return Err(timed(not_extended()));
dfa38cf4cd4batagen2026-10-10 09:17 UTC506
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC507
        if total < dc {
dfa38cf4cd4batagen2026-10-10 09:17 UTC508
            return Ok(Outcome::empty(StatusCode::NO_CONTENT).header(SERVER_TIMING, timing));
dfa38cf4cd4batagen2026-10-10 09:17 UTC509
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC510
dfa38cf4cd4batagen2026-10-10 09:17 UTC511
        let outcome = if self.eq < 50 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC512
            self.gain(length / 2.0, girth / 2.0, profile.foreskin_growth);
dfa38cf4cd4batagen2026-10-10 09:17 UTC513
            Outcome::json(
dfa38cf4cd4batagen2026-10-10 09:17 UTC514
                StatusCode::PARTIAL_CONTENT,
dfa38cf4cd4batagen2026-10-10 09:17 UTC515
                json!({
dfa38cf4cd4batagen2026-10-10 09:17 UTC516
                    "detail": "Partial gains were applied. EQ was below the band.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC517
                    "length": round3(length / 2.0),
dfa38cf4cd4batagen2026-10-10 09:17 UTC518
                    "girth": round3(girth / 2.0),
dfa38cf4cd4batagen2026-10-10 09:17 UTC519
                }),
dfa38cf4cd4batagen2026-10-10 09:17 UTC520
            )
dfa38cf4cd4batagen2026-10-10 09:17 UTC521
        } else if total >= dc + 5 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC522
            self.gain(length, girth, profile.foreskin_growth);
dfa38cf4cd4batagen2026-10-10 09:17 UTC523
            Outcome::json(
dfa38cf4cd4batagen2026-10-10 09:17 UTC524
                StatusCode::OK,
dfa38cf4cd4batagen2026-10-10 09:17 UTC525
                json!({
dfa38cf4cd4batagen2026-10-10 09:17 UTC526
                    "detail": "Gains were applied.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC527
                    "length": round3(length),
dfa38cf4cd4batagen2026-10-10 09:17 UTC528
                    "girth": round3(girth),
dfa38cf4cd4batagen2026-10-10 09:17 UTC529
                }),
dfa38cf4cd4batagen2026-10-10 09:17 UTC530
            )
dfa38cf4cd4batagen2026-10-10 09:17 UTC531
        } else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC532
            let eta = self.rng.random_range(1..=4);
dfa38cf4cd4batagen2026-10-10 09:17 UTC533
            self.pending.push(super::Pending {
dfa38cf4cd4batagen2026-10-10 09:17 UTC534
                due: self.turn + eta,
dfa38cf4cd4batagen2026-10-10 09:17 UTC535
                length,
dfa38cf4cd4batagen2026-10-10 09:17 UTC536
                girth,
dfa38cf4cd4batagen2026-10-10 09:17 UTC537
            });
dfa38cf4cd4batagen2026-10-10 09:17 UTC538
            self.gain(0.0, 0.0, profile.foreskin_growth);
dfa38cf4cd4batagen2026-10-10 09:17 UTC539
            Outcome::json(
dfa38cf4cd4batagen2026-10-10 09:17 UTC540
                StatusCode::ACCEPTED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC541
                json!({
dfa38cf4cd4batagen2026-10-10 09:17 UTC542
                    "detail": "Gains have been queued. They may arrive.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC543
                    "eta_turns": eta,
dfa38cf4cd4batagen2026-10-10 09:17 UTC544
                }),
dfa38cf4cd4batagen2026-10-10 09:17 UTC545
            )
dfa38cf4cd4batagen2026-10-10 09:17 UTC546
        };
dfa38cf4cd4batagen2026-10-10 09:17 UTC547
        Ok(outcome.header(SERVER_TIMING, timing))
dfa38cf4cd4batagen2026-10-10 09:17 UTC548
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC549
dfa38cf4cd4batagen2026-10-10 09:17 UTC550
    fn gossip(&mut self, outcome: Outcome) -> Outcome {
dfa38cf4cd4batagen2026-10-10 09:17 UTC551
        let reportable = matches!(outcome.status.as_u16(), 200 | 202 | 206);
dfa38cf4cd4batagen2026-10-10 09:17 UTC552
        if !self.roommate_told || !reportable {
dfa38cf4cd4batagen2026-10-10 09:17 UTC553
            return outcome;
dfa38cf4cd4batagen2026-10-10 09:17 UTC554
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC555
        self.roommate_told = false;
dfa38cf4cd4batagen2026-10-10 09:17 UTC556
        let mut outcome = outcome;
dfa38cf4cd4batagen2026-10-10 09:17 UTC557
        outcome.status = StatusCode::ALREADY_REPORTED;
dfa38cf4cd4batagen2026-10-10 09:17 UTC558
        if let super::Payload::Json(body) = &mut outcome.payload {
dfa38cf4cd4batagen2026-10-10 09:17 UTC559
            body["detail"] = json!("The roommate has already told everyone.");
dfa38cf4cd4batagen2026-10-10 09:17 UTC560
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC561
        outcome
dfa38cf4cd4batagen2026-10-10 09:17 UTC562
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC563
dfa38cf4cd4batagen2026-10-10 09:17 UTC564
    fn warm_up(&mut self) -> Result<Outcome, Problem> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC565
        if self.floor == Floor::Hotel && self.rng.random_ratio(1, 3) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC566
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC567
                StatusCode::SERVICE_UNAVAILABLE,
dfa38cf4cd4batagen2026-10-10 09:17 UTC568
                "service-unavailable",
dfa38cf4cd4batagen2026-10-10 09:17 UTC569
                "Service is unavailable. The server estimates next week. The server is being pessimistic.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC570
            )
dfa38cf4cd4batagen2026-10-10 09:17 UTC571
            .header(
dfa38cf4cd4batagen2026-10-10 09:17 UTC572
                header::RETRY_AFTER,
dfa38cf4cd4batagen2026-10-10 09:17 UTC573
                httpdate::fmt_http_date(SystemTime::now() + WEEK),
dfa38cf4cd4batagen2026-10-10 09:17 UTC574
            ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC575
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC576
        let (boost, detail) = if self.floor == Floor::Bathroom {
dfa38cf4cd4batagen2026-10-10 09:17 UTC577
            (
dfa38cf4cd4batagen2026-10-10 09:17 UTC578
                35,
dfa38cf4cd4batagen2026-10-10 09:17 UTC579
                "Warmed up. The shower is hot. The roommate's will not be.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC580
            )
c3e0c27cf8fdatagen2026-10-10 11:28 UTC581
        } else if self.towel_stolen {
c3e0c27cf8fdatagen2026-10-10 11:28 UTC582
            (10, "Warmed up, poorly. The cat has the towel.")
dfa38cf4cd4batagen2026-10-10 09:17 UTC583
        } else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC584
            (25, "Warmed up. The towel has cooled.")
dfa38cf4cd4batagen2026-10-10 09:17 UTC585
        };
dfa38cf4cd4batagen2026-10-10 09:17 UTC586
        self.touch();
dfa38cf4cd4batagen2026-10-10 09:17 UTC587
        self.warmed_up = true;
dfa38cf4cd4batagen2026-10-10 09:17 UTC588
        self.eq = (self.eq + boost).min(100);
dfa38cf4cd4batagen2026-10-10 09:17 UTC589
        Ok(Outcome::json(StatusCode::OK, json!({ "detail": detail })))
dfa38cf4cd4batagen2026-10-10 09:17 UTC590
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC591
dfa38cf4cd4batagen2026-10-10 09:17 UTC592
    fn ice(&mut self) -> Result<Outcome, Problem> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC593
        if self.items.ice_packs == 0 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC594
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC595
                StatusCode::FAILED_DEPENDENCY,
dfa38cf4cd4batagen2026-10-10 09:17 UTC596
                "no-ice",
dfa38cf4cd4batagen2026-10-10 09:17 UTC597
                "The ice technique depends on an ice pack. There are none.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC598
            ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC599
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC600
        self.touch();
dfa38cf4cd4batagen2026-10-10 09:17 UTC601
        self.items.ice_packs -= 1;
c3e0c27cf8fdatagen2026-10-10 11:28 UTC602
        if self.priapism_turns > 0 {
c3e0c27cf8fdatagen2026-10-10 11:28 UTC603
            return Ok(Outcome::json(
c3e0c27cf8fdatagen2026-10-10 11:28 UTC604
                StatusCode::OK,
c3e0c27cf8fdatagen2026-10-10 11:28 UTC605
                json!({ "detail": "The ice has no effect. The condition persists. The ice pack is gone." }),
c3e0c27cf8fdatagen2026-10-10 11:28 UTC606
            ));
c3e0c27cf8fdatagen2026-10-10 11:28 UTC607
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC608
        self.eq = (self.eq - 40).max(0);
dfa38cf4cd4batagen2026-10-10 09:17 UTC609
        Ok(Outcome::json(
dfa38cf4cd4batagen2026-10-10 09:17 UTC610
            StatusCode::OK,
dfa38cf4cd4batagen2026-10-10 09:17 UTC611
            json!({ "detail": "EQ has been reduced. The ice pack is gone." }),
dfa38cf4cd4batagen2026-10-10 09:17 UTC612
        ))
dfa38cf4cd4batagen2026-10-10 09:17 UTC613
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC614
dfa38cf4cd4batagen2026-10-10 09:17 UTC615
    fn supplement(&mut self) -> Result<Outcome, Problem> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC616
        if self.items.supplements == 0 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC617
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC618
                StatusCode::FAILED_DEPENDENCY,
dfa38cf4cd4batagen2026-10-10 09:17 UTC619
                "no-supplement",
dfa38cf4cd4batagen2026-10-10 09:17 UTC620
                "The supplement technique depends on a supplement.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC621
            ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC622
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC623
        self.items.supplements -= 1;
dfa38cf4cd4batagen2026-10-10 09:17 UTC624
        self.touch();
c3e0c27cf8fdatagen2026-10-10 11:28 UTC625
        if self.rng.random_ratio(1, 5) {
c3e0c27cf8fdatagen2026-10-10 11:28 UTC626
            self.priapism_turns = PRIAPISM_TURNS;
c3e0c27cf8fdatagen2026-10-10 11:28 UTC627
            self.eq = self.eq.max(PRIAPISM_EQ);
c3e0c27cf8fdatagen2026-10-10 11:28 UTC628
            return Ok(Outcome::json(
c3e0c27cf8fdatagen2026-10-10 11:28 UTC629
                StatusCode::OK,
c3e0c27cf8fdatagen2026-10-10 11:28 UTC630
                json!({ "detail": "The upstream returned a valid response. It is still returning it." }),
c3e0c27cf8fdatagen2026-10-10 11:28 UTC631
            ));
c3e0c27cf8fdatagen2026-10-10 11:28 UTC632
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC633
        Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC634
            StatusCode::BAD_GATEWAY,
dfa38cf4cd4batagen2026-10-10 09:17 UTC635
            "bad-gateway",
dfa38cf4cd4batagen2026-10-10 09:17 UTC636
            "The upstream returned an invalid response.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC637
        )
dfa38cf4cd4batagen2026-10-10 09:17 UTC638
        .with("upstream", "Pump-Bro"))
dfa38cf4cd4batagen2026-10-10 09:17 UTC639
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC640
dfa38cf4cd4batagen2026-10-10 09:17 UTC641
    fn edge(&mut self, now: Instant) -> Result<Outcome, Problem> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC642
        self.commit(5, None, now, 1);
dfa38cf4cd4batagen2026-10-10 09:17 UTC643
        self.edges += 1;
dfa38cf4cd4batagen2026-10-10 09:17 UTC644
        self.eq = (self.eq + 11).min(100);
dfa38cf4cd4batagen2026-10-10 09:17 UTC645
        self.girth += 0.004;
dfa38cf4cd4batagen2026-10-10 09:17 UTC646
        if self.edges >= 3 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC647
            self.edges = 0;
dfa38cf4cd4batagen2026-10-10 09:17 UTC648
            self.stamina = 0;
99524cf9f17catagen2026-10-10 10:53 UTC649
            self.refractory_until = Some(now + *REFRACTORY);
dfa38cf4cd4batagen2026-10-10 09:17 UTC650
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC651
                StatusCode::LOOP_DETECTED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC652
                "edging-loop",
dfa38cf4cd4batagen2026-10-10 09:17 UTC653
                "Edging has looped. The server detected an infinite loop and terminated it.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC654
            ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC655
        }
c3e0c27cf8fdatagen2026-10-10 11:28 UTC656
        if let Some(interrupted) = self.hazards(5)? {
c3e0c27cf8fdatagen2026-10-10 11:28 UTC657
            return Ok(interrupted);
c3e0c27cf8fdatagen2026-10-10 11:28 UTC658
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC659
        Ok(Outcome::json(
dfa38cf4cd4batagen2026-10-10 09:17 UTC660
            StatusCode::OK,
dfa38cf4cd4batagen2026-10-10 09:17 UTC661
            json!({ "detail": "Edged. EQ is rising. Nothing else is.", "edges": self.edges }),
dfa38cf4cd4batagen2026-10-10 09:17 UTC662
        ))
dfa38cf4cd4batagen2026-10-10 09:17 UTC663
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC664
dfa38cf4cd4batagen2026-10-10 09:17 UTC665
    fn pump(&mut self, now: Instant) -> Result<Outcome, Problem> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC666
        self.commit(4, None, now, 1);
c3e0c27cf8fdatagen2026-10-10 11:28 UTC667
        if let Some(interrupted) = self.hazards(4)? {
c3e0c27cf8fdatagen2026-10-10 11:28 UTC668
            return Ok(interrupted);
c3e0c27cf8fdatagen2026-10-10 11:28 UTC669
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC670
        self.temp_length += 0.5;
dfa38cf4cd4batagen2026-10-10 09:17 UTC671
        Ok(Outcome::json(
dfa38cf4cd4batagen2026-10-10 09:17 UTC672
            StatusCode::TEMPORARY_REDIRECT,
dfa38cf4cd4batagen2026-10-10 09:17 UTC673
            json!({ "detail": "Gains applied. They are temporary.", "length": 0.5 }),
dfa38cf4cd4batagen2026-10-10 09:17 UTC674
        )
dfa38cf4cd4batagen2026-10-10 09:17 UTC675
        .header(header::LOCATION, format!("/runs/{}/length", self.id)))
dfa38cf4cd4batagen2026-10-10 09:17 UTC676
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC677
}