atagen

jelq

public

an http based roguelike rpg about jelqing

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