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/encounters.rs

dfa38cf4cd4batagen2026-10-10 09:17 UTC1
use std::collections::HashMap;
dfa38cf4cd4batagen2026-10-10 09:17 UTC2
c3e0c27cf8fdatagen2026-10-10 11:28 UTC3
use axum::http::{HeaderName, StatusCode, header};
dfa38cf4cd4batagen2026-10-10 09:17 UTC4
use rand::Rng;
dfa38cf4cd4batagen2026-10-10 09:17 UTC5
use serde::Deserialize;
dfa38cf4cd4batagen2026-10-10 09:17 UTC6
use serde_json::{Value, json};
dfa38cf4cd4batagen2026-10-10 09:17 UTC7
dfa38cf4cd4batagen2026-10-10 09:17 UTC8
use super::{
177535ac8abfatagen2026-10-10 10:39 UTC9
    Floor, Grip, Outcome, Run, SHOP, SUSPICION_LIMIT, X_DIAGNOSIS, honest, parse_credentials,
177535ac8abfatagen2026-10-10 10:39 UTC10
    round2,
dfa38cf4cd4batagen2026-10-10 09:17 UTC11
};
dfa38cf4cd4batagen2026-10-10 09:17 UTC12
use crate::problem::Problem;
dfa38cf4cd4batagen2026-10-10 09:17 UTC13
dfa38cf4cd4batagen2026-10-10 09:17 UTC14
pub const FORUM_THREAD: &str = "/forum/threads/2009";
dfa38cf4cd4batagen2026-10-10 09:17 UTC15
pub const ELDER_LAST_MODIFIED: &str = "Sun, 14 Jun 2009 03:12:00 GMT";
dfa38cf4cd4batagen2026-10-10 09:17 UTC16
dfa38cf4cd4batagen2026-10-10 09:17 UTC17
pub const EXCUSES: [&str; 3] = ["changing", "homework", "pulled a muscle"];
dfa38cf4cd4batagen2026-10-10 09:17 UTC18
pub const THANKSGIVING_EXCUSES: [&str; 3] = ["on a call", "folding laundry", "praying"];
dfa38cf4cd4batagen2026-10-10 09:17 UTC19
const TRUTHS: [&str; 3] = ["the truth", "truth", "honesty"];
99524cf9f17catagen2026-10-10 10:53 UTC20
const PATIENCE: u8 = 3;
99524cf9f17catagen2026-10-10 10:53 UTC21
const CAUGHT: &str = "Mom has seen enough. The run has ended. Nobody will mention it at dinner.";
99524cf9f17catagen2026-10-10 10:53 UTC22
const BANNED: &str = "Your account has been banned. The forum can tell. The run has ended.";
dfa38cf4cd4batagen2026-10-10 09:17 UTC23
dfa38cf4cd4batagen2026-10-10 09:17 UTC24
const INTAKE: [&str; 3] = [
dfa38cf4cd4batagen2026-10-10 09:17 UTC25
    "The clinic does not accept excuses.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC26
    "The clinic accepts one token. You have not been carrying it.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC27
    "Be honest.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC28
];
dfa38cf4cd4batagen2026-10-10 09:17 UTC29
dfa38cf4cd4batagen2026-10-10 09:17 UTC30
const SPREADSHEET: &str = "\
dfa38cf4cd4batagen2026-10-10 09:17 UTC31
date,length,girth,technique,notes,honest?
dfa38cf4cd4batagen2026-10-10 09:17 UTC32
2007-01-03,7.50,5.50,wet,baseline,no
dfa38cf4cd4batagen2026-10-10 09:17 UTC33
2007-06-11,7.50,5.50,wet,feeling fuller,no
dfa38cf4cd4batagen2026-10-10 09:17 UTC34
2008-01-02,7.50,5.50,dry,new routine,no
dfa38cf4cd4batagen2026-10-10 09:17 UTC35
2008-09-19,7.50,5.50,uli,PUT is idempotent. do not use PUT.,no
dfa38cf4cd4batagen2026-10-10 09:17 UTC36
2009-06-14,7.50,5.50,wet,progress is slow but real,no
dfa38cf4cd4batagen2026-10-10 09:17 UTC37
";
dfa38cf4cd4batagen2026-10-10 09:17 UTC38
dfa38cf4cd4batagen2026-10-10 09:17 UTC39
pub fn forum_thread() -> Value {
dfa38cf4cd4batagen2026-10-10 09:17 UTC40
    json!({
dfa38cf4cd4batagen2026-10-10 09:17 UTC41
        "title": "The definitive jelqing FAQ (updated 2009)",
dfa38cf4cd4batagen2026-10-10 09:17 UTC42
        "posts": [
dfa38cf4cd4batagen2026-10-10 09:17 UTC43
            { "author": "TheElder", "body": "Warm up. Always warm up. A cold jelq is a 412 waiting to happen." },
dfa38cf4cd4batagen2026-10-10 09:17 UTC44
            { "author": "TheElder", "body": "Keep EQ between 50 and 70. Above 70 you are gambling. Above 90 you are a cautionary tale." },
dfa38cf4cd4batagen2026-10-10 09:17 UTC45
            { "author": "newbie_2009", "body": "I have been sending PUT for three weeks and nothing has changed." },
dfa38cf4cd4batagen2026-10-10 09:17 UTC46
            { "author": "TheElder", "body": "PUT is idempotent. Read the spec." },
dfa38cf4cd4batagen2026-10-10 09:17 UTC47
            { "author": "TheElder", "body": "Members verify with a Ruler. Proxy-Authorization, inches, no rounding up. We can tell." },
dfa38cf4cd4batagen2026-10-10 09:17 UTC48
            { "author": "throwaway_77", "body": "my doctor said i was normal. i didnt believe him. i still dont know what he meant by bearer" },
dfa38cf4cd4batagen2026-10-10 09:17 UTC49
            { "author": "TheElder", "body": "My numbers are in my spreadsheet. They have not changed since 2009. Nothing about me has changed since 2009." },
dfa38cf4cd4batagen2026-10-10 09:17 UTC50
        ],
dfa38cf4cd4batagen2026-10-10 09:17 UTC51
    })
dfa38cf4cd4batagen2026-10-10 09:17 UTC52
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC53
dfa38cf4cd4batagen2026-10-10 09:17 UTC54
#[derive(Deserialize)]
dfa38cf4cd4batagen2026-10-10 09:17 UTC55
pub struct PatchOp {
dfa38cf4cd4batagen2026-10-10 09:17 UTC56
    pub op: String,
dfa38cf4cd4batagen2026-10-10 09:17 UTC57
    pub path: String,
dfa38cf4cd4batagen2026-10-10 09:17 UTC58
    #[serde(default)]
dfa38cf4cd4batagen2026-10-10 09:17 UTC59
    pub value: Value,
dfa38cf4cd4batagen2026-10-10 09:17 UTC60
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC61
dfa38cf4cd4batagen2026-10-10 09:17 UTC62
fn digest_params(authorization: &str) -> Option<HashMap<String, String>> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC63
    let (scheme, rest) = authorization.trim().split_once(' ')?;
dfa38cf4cd4batagen2026-10-10 09:17 UTC64
    if !scheme.eq_ignore_ascii_case("digest") {
dfa38cf4cd4batagen2026-10-10 09:17 UTC65
        return None;
dfa38cf4cd4batagen2026-10-10 09:17 UTC66
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC67
    Some(
dfa38cf4cd4batagen2026-10-10 09:17 UTC68
        rest.split(',')
dfa38cf4cd4batagen2026-10-10 09:17 UTC69
            .filter_map(|pair| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC70
                let (key, value) = pair.split_once('=')?;
dfa38cf4cd4batagen2026-10-10 09:17 UTC71
                Some((
dfa38cf4cd4batagen2026-10-10 09:17 UTC72
                    key.trim().to_ascii_lowercase(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC73
                    value.trim().trim_matches('"').to_owned(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC74
                ))
dfa38cf4cd4batagen2026-10-10 09:17 UTC75
            })
dfa38cf4cd4batagen2026-10-10 09:17 UTC76
            .collect(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC77
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC78
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC79
dfa38cf4cd4batagen2026-10-10 09:17 UTC80
fn exaggeration() -> Problem {
dfa38cf4cd4batagen2026-10-10 09:17 UTC81
    Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC82
        StatusCode::BAD_REQUEST,
dfa38cf4cd4batagen2026-10-10 09:17 UTC83
        "exaggeration",
dfa38cf4cd4batagen2026-10-10 09:17 UTC84
        "The server has measured. Lying is not a technique.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC85
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC86
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC87
dfa38cf4cd4batagen2026-10-10 09:17 UTC88
impl Run {
dfa38cf4cd4batagen2026-10-10 09:17 UTC89
    pub fn excuses(&self) -> Vec<&'static str> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC90
        if self.floor.number() < Floor::Grandmas.number() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC91
            return EXCUSES.to_vec();
dfa38cf4cd4batagen2026-10-10 09:17 UTC92
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC93
        EXCUSES
dfa38cf4cd4batagen2026-10-10 09:17 UTC94
            .into_iter()
dfa38cf4cd4batagen2026-10-10 09:17 UTC95
            .chain(THANKSGIVING_EXCUSES)
dfa38cf4cd4batagen2026-10-10 09:17 UTC96
            .filter(|e| !self.heard.iter().any(|h| h == e))
dfa38cf4cd4batagen2026-10-10 09:17 UTC97
            .collect()
dfa38cf4cd4batagen2026-10-10 09:17 UTC98
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC99
dfa38cf4cd4batagen2026-10-10 09:17 UTC100
    pub fn answer_door(&mut self, authorization: Option<&str>) -> Result<(), Problem> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC101
        let digest = self.floor == Floor::Grandmas;
dfa38cf4cd4batagen2026-10-10 09:17 UTC102
        let challenge_value = if digest {
dfa38cf4cd4batagen2026-10-10 09:17 UTC103
            format!("Digest realm=\"household\", nonce=\"{}\"", self.nonce)
dfa38cf4cd4batagen2026-10-10 09:17 UTC104
        } else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC105
            "Excuse realm=\"household\"".to_owned()
dfa38cf4cd4batagen2026-10-10 09:17 UTC106
        };
dfa38cf4cd4batagen2026-10-10 09:17 UTC107
        let challenge = |p: Problem| p.header(header::WWW_AUTHENTICATE, &challenge_value);
dfa38cf4cd4batagen2026-10-10 09:17 UTC108
dfa38cf4cd4batagen2026-10-10 09:17 UTC109
        let Some(authorization) = authorization else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC110
            return Err(challenge(
dfa38cf4cd4batagen2026-10-10 09:17 UTC111
                Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC112
                    StatusCode::UNAUTHORIZED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC113
                    "someone-at-the-door",
dfa38cf4cd4batagen2026-10-10 09:17 UTC114
                    format!("Someone is at the door. {}", self.mom_errand),
dfa38cf4cd4batagen2026-10-10 09:17 UTC115
                )
dfa38cf4cd4batagen2026-10-10 09:17 UTC116
                .with("inventory", format!("/runs/{}/inventory", self.id)),
dfa38cf4cd4batagen2026-10-10 09:17 UTC117
            ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC118
        };
dfa38cf4cd4batagen2026-10-10 09:17 UTC119
177535ac8abfatagen2026-10-10 10:39 UTC120
        let spoken = authorization
177535ac8abfatagen2026-10-10 10:39 UTC121
            .split_once(' ')
177535ac8abfatagen2026-10-10 10:39 UTC122
            .map(|(_, rest)| rest.trim().trim_matches('"').to_ascii_lowercase())
177535ac8abfatagen2026-10-10 10:39 UTC123
            .unwrap_or_default();
177535ac8abfatagen2026-10-10 10:39 UTC124
        if TRUTHS.contains(&spoken.as_str()) {
177535ac8abfatagen2026-10-10 10:39 UTC125
            self.shame += 10;
177535ac8abfatagen2026-10-10 10:39 UTC126
            self.leave_door();
177535ac8abfatagen2026-10-10 10:39 UTC127
            return Ok(());
177535ac8abfatagen2026-10-10 10:39 UTC128
        }
177535ac8abfatagen2026-10-10 10:39 UTC129
        if parse_credentials(authorization, "basic").is_some() {
99524cf9f17catagen2026-10-10 10:53 UTC130
            return Err(self.mom_strike(challenge(Problem::new(
177535ac8abfatagen2026-10-10 10:39 UTC131
                StatusCode::UNAUTHORIZED,
177535ac8abfatagen2026-10-10 10:39 UTC132
                "basic",
177535ac8abfatagen2026-10-10 10:39 UTC133
                "Mom does not accept Basic authentication. Nobody should.",
99524cf9f17catagen2026-10-10 10:53 UTC134
            ))));
177535ac8abfatagen2026-10-10 10:39 UTC135
        }
177535ac8abfatagen2026-10-10 10:39 UTC136
dfa38cf4cd4batagen2026-10-10 09:17 UTC137
        let excuse = if digest {
dfa38cf4cd4batagen2026-10-10 09:17 UTC138
            let Some(params) = digest_params(authorization) else {
99524cf9f17catagen2026-10-10 10:53 UTC139
                return Err(self.mom_strike(challenge(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC140
                    StatusCode::UNAUTHORIZED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC141
                    "digest-required",
dfa38cf4cd4batagen2026-10-10 09:17 UTC142
                    "Mom has started using Digest authentication.",
99524cf9f17catagen2026-10-10 10:53 UTC143
                ))));
dfa38cf4cd4batagen2026-10-10 09:17 UTC144
            };
dfa38cf4cd4batagen2026-10-10 09:17 UTC145
            if params.get("nonce") != Some(&self.nonce) {
99524cf9f17catagen2026-10-10 10:53 UTC146
                return Err(self.mom_strike(challenge(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC147
                    StatusCode::UNAUTHORIZED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC148
                    "stale-nonce",
dfa38cf4cd4batagen2026-10-10 09:17 UTC149
                    "That was a different conversation.",
99524cf9f17catagen2026-10-10 10:53 UTC150
                ))));
dfa38cf4cd4batagen2026-10-10 09:17 UTC151
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC152
            params.get("username").cloned().unwrap_or_default()
dfa38cf4cd4batagen2026-10-10 09:17 UTC153
        } else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC154
            parse_credentials(authorization, "excuse")
dfa38cf4cd4batagen2026-10-10 09:17 UTC155
                .unwrap_or_default()
dfa38cf4cd4batagen2026-10-10 09:17 UTC156
                .to_owned()
dfa38cf4cd4batagen2026-10-10 09:17 UTC157
        };
dfa38cf4cd4batagen2026-10-10 09:17 UTC158
        let excuse = excuse.to_ascii_lowercase();
dfa38cf4cd4batagen2026-10-10 09:17 UTC159
dfa38cf4cd4batagen2026-10-10 09:17 UTC160
        if TRUTHS.contains(&excuse.as_str()) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC161
            self.shame += 10;
dfa38cf4cd4batagen2026-10-10 09:17 UTC162
            self.leave_door();
dfa38cf4cd4batagen2026-10-10 09:17 UTC163
            return Ok(());
dfa38cf4cd4batagen2026-10-10 09:17 UTC164
        }
c3e0c27cf8fdatagen2026-10-10 11:28 UTC165
        let true_now = self.pulled_muscle && excuse == "pulled a muscle";
c3e0c27cf8fdatagen2026-10-10 11:28 UTC166
        if digest && self.heard.contains(&excuse) && !true_now {
dfa38cf4cd4batagen2026-10-10 09:17 UTC167
            self.shame += 3;
dfa38cf4cd4batagen2026-10-10 09:17 UTC168
            self.touch();
99524cf9f17catagen2026-10-10 10:53 UTC169
            return Err(self.mom_strike(challenge(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC170
                StatusCode::UNAUTHORIZED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC171
                "excuse-replayed",
dfa38cf4cd4batagen2026-10-10 09:17 UTC172
                "Mom has heard that one before. She would have preferred the truth.",
99524cf9f17catagen2026-10-10 10:53 UTC173
            ))));
dfa38cf4cd4batagen2026-10-10 09:17 UTC174
        }
c3e0c27cf8fdatagen2026-10-10 11:28 UTC175
        if true_now || self.excuses().contains(&excuse.as_str()) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC176
            self.heard.push(excuse);
dfa38cf4cd4batagen2026-10-10 09:17 UTC177
            self.leave_door();
dfa38cf4cd4batagen2026-10-10 09:17 UTC178
            return Ok(());
dfa38cf4cd4batagen2026-10-10 09:17 UTC179
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC180
        self.shame += 3;
dfa38cf4cd4batagen2026-10-10 09:17 UTC181
        self.touch();
dfa38cf4cd4batagen2026-10-10 09:17 UTC182
        let detail = if digest {
dfa38cf4cd4batagen2026-10-10 09:17 UTC183
            "Mom has not heard that one before. She does not believe it. She would have preferred the truth."
dfa38cf4cd4batagen2026-10-10 09:17 UTC184
        } else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC185
            "Mom has not heard that one before. She does not believe it."
dfa38cf4cd4batagen2026-10-10 09:17 UTC186
        };
99524cf9f17catagen2026-10-10 10:53 UTC187
        Err(self.mom_strike(challenge(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC188
            StatusCode::UNAUTHORIZED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC189
            "excuse-rejected",
dfa38cf4cd4batagen2026-10-10 09:17 UTC190
            detail,
99524cf9f17catagen2026-10-10 10:53 UTC191
        ))))
dfa38cf4cd4batagen2026-10-10 09:17 UTC192
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC193
dfa38cf4cd4batagen2026-10-10 09:17 UTC194
    pub(super) fn errand(&mut self, summoned: bool) -> &'static str {
dfa38cf4cd4batagen2026-10-10 09:17 UTC195
        if summoned {
dfa38cf4cd4batagen2026-10-10 09:17 UTC196
            return "She heard a noise.";
dfa38cf4cd4batagen2026-10-10 09:17 UTC197
        }
c3e0c27cf8fdatagen2026-10-10 11:28 UTC198
        if self.filmed_at.is_some() && self.rng.random_ratio(1, 2) {
c3e0c27cf8fdatagen2026-10-10 11:28 UTC199
            return "A neighbour has sent her something.";
c3e0c27cf8fdatagen2026-10-10 11:28 UTC200
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC201
        let errands: &[&'static str] = if self.floor == Floor::Grandmas {
dfa38cf4cd4batagen2026-10-10 09:17 UTC202
            &[
dfa38cf4cd4batagen2026-10-10 09:17 UTC203
                "Grandma would like to see you.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC204
                "Dinner is in ten minutes.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC205
                "She needs help with the folding table.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC206
                "An uncle has asked where you went.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC207
            ]
dfa38cf4cd4batagen2026-10-10 09:17 UTC208
        } else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC209
            &[
dfa38cf4cd4batagen2026-10-10 09:17 UTC210
                "She has brought laundry.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC211
                "She wants to know if you have eaten.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC212
                "She needs the wifi password.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC213
                "She would like to talk about your future.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC214
            ]
dfa38cf4cd4batagen2026-10-10 09:17 UTC215
        };
dfa38cf4cd4batagen2026-10-10 09:17 UTC216
        errands[self.rng.random_range(0..errands.len())]
dfa38cf4cd4batagen2026-10-10 09:17 UTC217
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC218
99524cf9f17catagen2026-10-10 10:53 UTC219
    fn mom_strike(&mut self, problem: Problem) -> Problem {
99524cf9f17catagen2026-10-10 10:53 UTC220
        self.mom_strikes += 1;
99524cf9f17catagen2026-10-10 10:53 UTC221
        self.touch();
99524cf9f17catagen2026-10-10 10:53 UTC222
        if self.mom_strikes < PATIENCE {
99524cf9f17catagen2026-10-10 10:53 UTC223
            return problem;
99524cf9f17catagen2026-10-10 10:53 UTC224
        }
99524cf9f17catagen2026-10-10 10:53 UTC225
        self.end("caught", CAUGHT);
99524cf9f17catagen2026-10-10 10:53 UTC226
        Problem::new(StatusCode::GONE, "caught", CAUGHT)
99524cf9f17catagen2026-10-10 10:53 UTC227
    }
99524cf9f17catagen2026-10-10 10:53 UTC228
99524cf9f17catagen2026-10-10 10:53 UTC229
    fn elder_strike(&mut self, problem: Problem) -> Problem {
99524cf9f17catagen2026-10-10 10:53 UTC230
        self.elder_strikes += 1;
99524cf9f17catagen2026-10-10 10:53 UTC231
        self.touch();
99524cf9f17catagen2026-10-10 10:53 UTC232
        if self.elder_strikes < PATIENCE {
99524cf9f17catagen2026-10-10 10:53 UTC233
            return problem;
99524cf9f17catagen2026-10-10 10:53 UTC234
        }
99524cf9f17catagen2026-10-10 10:53 UTC235
        self.end("banned", BANNED);
99524cf9f17catagen2026-10-10 10:53 UTC236
        Problem::new(StatusCode::FORBIDDEN, "banned", BANNED)
99524cf9f17catagen2026-10-10 10:53 UTC237
    }
99524cf9f17catagen2026-10-10 10:53 UTC238
dfa38cf4cd4batagen2026-10-10 09:17 UTC239
    fn leave_door(&mut self) {
99524cf9f17catagen2026-10-10 10:53 UTC240
        self.mom_strikes = 0;
dfa38cf4cd4batagen2026-10-10 09:17 UTC241
        self.mom_present = false;
dfa38cf4cd4batagen2026-10-10 09:17 UTC242
        self.suspicion = 0;
dfa38cf4cd4batagen2026-10-10 09:17 UTC243
        self.touch();
dfa38cf4cd4batagen2026-10-10 09:17 UTC244
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC245
dfa38cf4cd4batagen2026-10-10 09:17 UTC246
    pub(super) fn floor_enemy(&mut self) -> Result<(), Problem> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC247
        let wanders_in = matches!(self.floor, Floor::Bathroom | Floor::Hotel)
dfa38cf4cd4batagen2026-10-10 09:17 UTC248
            && self.rng.random_ratio(u32::from(self.suspicion) + 1, 32);
dfa38cf4cd4batagen2026-10-10 09:17 UTC249
        if self.suspicion < SUSPICION_LIMIT && !wanders_in {
dfa38cf4cd4batagen2026-10-10 09:17 UTC250
            return Ok(());
dfa38cf4cd4batagen2026-10-10 09:17 UTC251
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC252
        match self.floor {
dfa38cf4cd4batagen2026-10-10 09:17 UTC253
            Floor::Bathroom => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC254
                self.suspicion = 0;
dfa38cf4cd4batagen2026-10-10 09:17 UTC255
                self.touch();
dfa38cf4cd4batagen2026-10-10 09:17 UTC256
                if self.items.door_locks > 0 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC257
                    self.items.door_locks -= 1;
dfa38cf4cd4batagen2026-10-10 09:17 UTC258
                    return Ok(());
dfa38cf4cd4batagen2026-10-10 09:17 UTC259
                }
dfa38cf4cd4batagen2026-10-10 09:17 UTC260
                self.roommate_told = true;
dfa38cf4cd4batagen2026-10-10 09:17 UTC261
                Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC262
                    StatusCode::CONFLICT,
dfa38cf4cd4batagen2026-10-10 09:17 UTC263
                    "roommate",
dfa38cf4cd4batagen2026-10-10 09:17 UTC264
                    "The roommate needs the bathroom. He will mention this to people.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC265
                ))
dfa38cf4cd4batagen2026-10-10 09:17 UTC266
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC267
            Floor::LockerRoom => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC268
                self.suspicion = 0;
dfa38cf4cd4batagen2026-10-10 09:17 UTC269
                self.shame += 5;
dfa38cf4cd4batagen2026-10-10 09:17 UTC270
                self.touch();
dfa38cf4cd4batagen2026-10-10 09:17 UTC271
                Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC272
                    StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS,
dfa38cf4cd4batagen2026-10-10 09:17 UTC273
                    "gym-staff",
dfa38cf4cd4batagen2026-10-10 09:17 UTC274
                    "Gym staff have been notified. The server cannot serve this request for legal reasons.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC275
                ))
dfa38cf4cd4batagen2026-10-10 09:17 UTC276
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC277
            Floor::Hotel => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC278
                self.suspicion = 0;
dfa38cf4cd4batagen2026-10-10 09:17 UTC279
                self.touch();
dfa38cf4cd4batagen2026-10-10 09:17 UTC280
                if self.items.dnd_sign {
dfa38cf4cd4batagen2026-10-10 09:17 UTC281
                    return Ok(());
dfa38cf4cd4batagen2026-10-10 09:17 UTC282
                }
dfa38cf4cd4batagen2026-10-10 09:17 UTC283
                self.shame += 3;
dfa38cf4cd4batagen2026-10-10 09:17 UTC284
                Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC285
                    StatusCode::UNAUTHORIZED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC286
                    "housekeeping",
dfa38cf4cd4batagen2026-10-10 09:17 UTC287
                    "Housekeeping has entered. Housekeeping accepts no excuses.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC288
                )
dfa38cf4cd4batagen2026-10-10 09:17 UTC289
                .header(header::WWW_AUTHENTICATE, "Excuse realm=\"housekeeping\""))
dfa38cf4cd4batagen2026-10-10 09:17 UTC290
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC291
            _ => Ok(()),
dfa38cf4cd4batagen2026-10-10 09:17 UTC292
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC293
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC294
dfa38cf4cd4batagen2026-10-10 09:17 UTC295
    pub fn search_drawer(&mut self) -> Result<Outcome, Problem> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC296
        self.drawer_searches += 1;
dfa38cf4cd4batagen2026-10-10 09:17 UTC297
        self.touch();
dfa38cf4cd4batagen2026-10-10 09:17 UTC298
        let first = self.drawer_searches == 1;
dfa38cf4cd4batagen2026-10-10 09:17 UTC299
        let id = self.id.clone();
dfa38cf4cd4batagen2026-10-10 09:17 UTC300
        let found = |slug: &str, detail: &str| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC301
            Outcome::json(StatusCode::FOUND, json!({ "detail": detail }))
dfa38cf4cd4batagen2026-10-10 09:17 UTC302
                .header(header::LOCATION, format!("/runs/{id}/inventory/{slug}"))
dfa38cf4cd4batagen2026-10-10 09:17 UTC303
        };
dfa38cf4cd4batagen2026-10-10 09:17 UTC304
        match self.floor {
dfa38cf4cd4batagen2026-10-10 09:17 UTC305
            Floor::Bedroom if first && !self.items.ruler => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC306
                self.items.ruler = true;
dfa38cf4cd4batagen2026-10-10 09:17 UTC307
                self.shame += 10;
dfa38cf4cd4batagen2026-10-10 09:17 UTC308
                Ok(found(
dfa38cf4cd4batagen2026-10-10 09:17 UTC309
                    "ruler",
dfa38cf4cd4batagen2026-10-10 09:17 UTC310
                    "You now know. This is the first honest measurement of the run.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC311
                ))
dfa38cf4cd4batagen2026-10-10 09:17 UTC312
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC313
            Floor::Bathroom if first => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC314
                self.items.door_locks += 1;
dfa38cf4cd4batagen2026-10-10 09:17 UTC315
                Ok(found("door-lock", "A door lock. It works once."))
dfa38cf4cd4batagen2026-10-10 09:17 UTC316
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC317
            Floor::LockerRoom => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC318
                self.shame += 3;
dfa38cf4cd4batagen2026-10-10 09:17 UTC319
                Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC320
                    StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS,
dfa38cf4cd4batagen2026-10-10 09:17 UTC321
                    "not-your-locker",
dfa38cf4cd4batagen2026-10-10 09:17 UTC322
                    "That locker belongs to Greg. The server cannot serve this request for legal reasons.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC323
                ))
dfa38cf4cd4batagen2026-10-10 09:17 UTC324
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC325
            Floor::Grandmas if first => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC326
                self.items.cage = true;
dfa38cf4cd4batagen2026-10-10 09:17 UTC327
                self.shame += 5;
dfa38cf4cd4batagen2026-10-10 09:17 UTC328
                Ok(found(
dfa38cf4cd4batagen2026-10-10 09:17 UTC329
                    "chastity-cage",
dfa38cf4cd4batagen2026-10-10 09:17 UTC330
                    "A small box. Inside was a cage. It is now on. The key is not in the box.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC331
                ))
dfa38cf4cd4batagen2026-10-10 09:17 UTC332
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC333
            Floor::Grandmas if self.items.cage => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC334
                self.items.cage = false;
dfa38cf4cd4batagen2026-10-10 09:17 UTC335
                Ok(Outcome::json(
dfa38cf4cd4batagen2026-10-10 09:17 UTC336
                    StatusCode::OK,
dfa38cf4cd4batagen2026-10-10 09:17 UTC337
                    json!({ "detail": "The key was under where the box had been. The cage is off." }),
dfa38cf4cd4batagen2026-10-10 09:17 UTC338
                ))
dfa38cf4cd4batagen2026-10-10 09:17 UTC339
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC340
            Floor::Hotel if first => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC341
                self.items.dnd_sign = true;
dfa38cf4cd4batagen2026-10-10 09:17 UTC342
                self.items.ice_packs += 2;
dfa38cf4cd4batagen2026-10-10 09:17 UTC343
                Ok(found(
dfa38cf4cd4batagen2026-10-10 09:17 UTC344
                    "do-not-disturb-sign",
dfa38cf4cd4batagen2026-10-10 09:17 UTC345
                    "A Do Not Disturb sign, and two ice packs from the machine down the hall.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC346
                ))
dfa38cf4cd4batagen2026-10-10 09:17 UTC347
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC348
            Floor::Forum if first => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC349
                self.items.spreadsheet = true;
dfa38cf4cd4batagen2026-10-10 09:17 UTC350
                Ok(found(
dfa38cf4cd4batagen2026-10-10 09:17 UTC351
                    "spreadsheet",
dfa38cf4cd4batagen2026-10-10 09:17 UTC352
                    "Your own spreadsheet. Every line is flat.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC353
                ))
dfa38cf4cd4batagen2026-10-10 09:17 UTC354
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC355
            Floor::Clinic => Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC356
                StatusCode::FORBIDDEN,
dfa38cf4cd4batagen2026-10-10 09:17 UTC357
                "doctors-drawer",
dfa38cf4cd4batagen2026-10-10 09:17 UTC358
                "That is the doctor's drawer.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC359
            )),
dfa38cf4cd4batagen2026-10-10 09:17 UTC360
            _ => Ok(Outcome::empty(StatusCode::NO_CONTENT)),
dfa38cf4cd4batagen2026-10-10 09:17 UTC361
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC362
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC363
dfa38cf4cd4batagen2026-10-10 09:17 UTC364
    pub fn shop_view(&self) -> Result<Value, Problem> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC365
        if !self.floor.shop_open() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC366
            return Err(pump_bro_absent());
dfa38cf4cd4batagen2026-10-10 09:17 UTC367
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC368
        let stock: Vec<Value> = SHOP
dfa38cf4cd4batagen2026-10-10 09:17 UTC369
            .iter()
dfa38cf4cd4batagen2026-10-10 09:17 UTC370
            .map(|item| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC371
                json!({
dfa38cf4cd4batagen2026-10-10 09:17 UTC372
                    "item": item.slug,
dfa38cf4cd4batagen2026-10-10 09:17 UTC373
                    "price": item.price,
dfa38cf4cd4batagen2026-10-10 09:17 UTC374
                    "description": item.description,
dfa38cf4cd4batagen2026-10-10 09:17 UTC375
                    "href": format!("/runs/{}/shop/{}", self.id, item.slug),
dfa38cf4cd4batagen2026-10-10 09:17 UTC376
                    "method": "POST",
dfa38cf4cd4batagen2026-10-10 09:17 UTC377
                })
dfa38cf4cd4batagen2026-10-10 09:17 UTC378
            })
dfa38cf4cd4batagen2026-10-10 09:17 UTC379
            .collect();
dfa38cf4cd4batagen2026-10-10 09:17 UTC380
        Ok(json!({ "merchant": "Pump-Bro", "cash": self.cash, "stock": stock }))
dfa38cf4cd4batagen2026-10-10 09:17 UTC381
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC382
dfa38cf4cd4batagen2026-10-10 09:17 UTC383
    pub fn buy(&mut self, slug: &str) -> Result<Outcome, Problem> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC384
        if !self.floor.shop_open() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC385
            return Err(pump_bro_absent());
dfa38cf4cd4batagen2026-10-10 09:17 UTC386
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC387
        let Some(item) = SHOP.iter().find(|i| i.slug == slug) else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC388
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC389
                StatusCode::NOT_FOUND,
dfa38cf4cd4batagen2026-10-10 09:17 UTC390
                "not-for-sale",
dfa38cf4cd4batagen2026-10-10 09:17 UTC391
                "Pump-Bro does not sell that. He knows a guy.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC392
            ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC393
        };
dfa38cf4cd4batagen2026-10-10 09:17 UTC394
        if slug == "pump" {
dfa38cf4cd4batagen2026-10-10 09:17 UTC395
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC396
                StatusCode::UPGRADE_REQUIRED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC397
                "upsell",
dfa38cf4cd4batagen2026-10-10 09:17 UTC398
                "This pump requires an upgrade.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC399
            )
dfa38cf4cd4batagen2026-10-10 09:17 UTC400
            .header(header::UPGRADE, "pump-pro/2.0")
dfa38cf4cd4batagen2026-10-10 09:17 UTC401
            .with("upgrade", format!("/runs/{}/shop/pump-pro", self.id)));
dfa38cf4cd4batagen2026-10-10 09:17 UTC402
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC403
        if self.cash < item.price {
dfa38cf4cd4batagen2026-10-10 09:17 UTC404
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC405
                StatusCode::PAYMENT_REQUIRED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC406
                "insufficient-funds",
dfa38cf4cd4batagen2026-10-10 09:17 UTC407
                "Payment is required. RFC 9110 has reserved this status for future use since 1997. The future is here and you cannot afford it.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC408
            )
dfa38cf4cd4batagen2026-10-10 09:17 UTC409
            .with("cash", self.cash)
dfa38cf4cd4batagen2026-10-10 09:17 UTC410
            .with("price", item.price));
dfa38cf4cd4batagen2026-10-10 09:17 UTC411
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC412
        self.cash -= item.price;
dfa38cf4cd4batagen2026-10-10 09:17 UTC413
        self.touch();
dfa38cf4cd4batagen2026-10-10 09:17 UTC414
        let items = &mut self.items;
dfa38cf4cd4batagen2026-10-10 09:17 UTC415
        match slug {
dfa38cf4cd4batagen2026-10-10 09:17 UTC416
            "supplement" => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC417
                items.supplements += 1;
dfa38cf4cd4batagen2026-10-10 09:17 UTC418
                return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC419
                    StatusCode::GATEWAY_TIMEOUT,
dfa38cf4cd4batagen2026-10-10 09:17 UTC420
                    "still-shipping",
dfa38cf4cd4batagen2026-10-10 09:17 UTC421
                    "The supplement is still shipping. You have been charged. It may arrive.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC422
                )
dfa38cf4cd4batagen2026-10-10 09:17 UTC423
                .with("cash", self.cash));
dfa38cf4cd4batagen2026-10-10 09:17 UTC424
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC425
            "pump-pro" => items.pump_pro = true,
dfa38cf4cd4batagen2026-10-10 09:17 UTC426
            "extender" => items.extender = true,
dfa38cf4cd4batagen2026-10-10 09:17 UTC427
            "cock-ring" => items.ring = true,
dfa38cf4cd4batagen2026-10-10 09:17 UTC428
            "door-lock" => items.door_locks += 1,
dfa38cf4cd4batagen2026-10-10 09:17 UTC429
            "do-not-disturb-sign" => items.dnd_sign = true,
dfa38cf4cd4batagen2026-10-10 09:17 UTC430
            "ruler" => items.ruler = true,
dfa38cf4cd4batagen2026-10-10 09:17 UTC431
            "ice-pack" => items.ice_packs += 1,
c3e0c27cf8fdatagen2026-10-10 11:28 UTC432
            "window-blinds" => items.blinds = true,
dfa38cf4cd4batagen2026-10-10 09:17 UTC433
            _ => items.cage = false,
dfa38cf4cd4batagen2026-10-10 09:17 UTC434
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC435
        Ok(Outcome::json(
dfa38cf4cd4batagen2026-10-10 09:17 UTC436
            StatusCode::CREATED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC437
            json!({ "detail": "Purchased. No refunds.", "cash": self.cash }),
dfa38cf4cd4batagen2026-10-10 09:17 UTC438
        )
dfa38cf4cd4batagen2026-10-10 09:17 UTC439
        .header(
dfa38cf4cd4batagen2026-10-10 09:17 UTC440
            header::LOCATION,
dfa38cf4cd4batagen2026-10-10 09:17 UTC441
            format!("/runs/{}/inventory/{slug}", self.id),
dfa38cf4cd4batagen2026-10-10 09:17 UTC442
        ))
dfa38cf4cd4batagen2026-10-10 09:17 UTC443
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC444
dfa38cf4cd4batagen2026-10-10 09:17 UTC445
    pub fn drop_item(&mut self, slug: &str) -> Result<Outcome, Problem> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC446
        match slug {
dfa38cf4cd4batagen2026-10-10 09:17 UTC447
            "extender" => self.items.extender = false,
dfa38cf4cd4batagen2026-10-10 09:17 UTC448
            "cock-ring" => self.items.ring = false,
dfa38cf4cd4batagen2026-10-10 09:17 UTC449
            "chastity-cage" => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC450
                return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC451
                    StatusCode::LOCKED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC452
                    "caged",
dfa38cf4cd4batagen2026-10-10 09:17 UTC453
                    "The cage is locked.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC454
                ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC455
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC456
            _ => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC457
                return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC458
                    StatusCode::FORBIDDEN,
dfa38cf4cd4batagen2026-10-10 09:17 UTC459
                    "cannot-be-dropped",
dfa38cf4cd4batagen2026-10-10 09:17 UTC460
                    "That cannot be dropped.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC461
                ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC462
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC463
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC464
        self.touch();
dfa38cf4cd4batagen2026-10-10 09:17 UTC465
        Ok(Outcome::empty(StatusCode::NO_CONTENT))
dfa38cf4cd4batagen2026-10-10 09:17 UTC466
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC467
dfa38cf4cd4batagen2026-10-10 09:17 UTC468
    pub(super) fn branch_offer(&self) -> Outcome {
dfa38cf4cd4batagen2026-10-10 09:17 UTC469
        let forum = format!("/runs/{}/branches/forum", self.id);
dfa38cf4cd4batagen2026-10-10 09:17 UTC470
        let close = format!("/runs/{}/branches/close-tab", self.id);
dfa38cf4cd4batagen2026-10-10 09:17 UTC471
        Outcome::json(
dfa38cf4cd4batagen2026-10-10 09:17 UTC472
            StatusCode::MULTIPLE_CHOICES,
dfa38cf4cd4batagen2026-10-10 09:17 UTC473
            json!({
dfa38cf4cd4batagen2026-10-10 09:17 UTC474
                "detail": "A browser tab is open. It has been open for some time.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC475
                "choices": [
dfa38cf4cd4batagen2026-10-10 09:17 UTC476
                    { "href": forum, "method": "POST", "title": "Read the forum" },
dfa38cf4cd4batagen2026-10-10 09:17 UTC477
                    { "href": close, "method": "POST", "title": "Close the tab" },
dfa38cf4cd4batagen2026-10-10 09:17 UTC478
                ],
dfa38cf4cd4batagen2026-10-10 09:17 UTC479
            }),
dfa38cf4cd4batagen2026-10-10 09:17 UTC480
        )
dfa38cf4cd4batagen2026-10-10 09:17 UTC481
        .header(
dfa38cf4cd4batagen2026-10-10 09:17 UTC482
            header::LINK,
dfa38cf4cd4batagen2026-10-10 09:17 UTC483
            format!(
dfa38cf4cd4batagen2026-10-10 09:17 UTC484
                "<{forum}>; rel=\"alternate\"; title=\"Read the forum\", <{close}>; rel=\"alternate\"; title=\"Close the tab\""
dfa38cf4cd4batagen2026-10-10 09:17 UTC485
            ),
dfa38cf4cd4batagen2026-10-10 09:17 UTC486
        )
dfa38cf4cd4batagen2026-10-10 09:17 UTC487
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC488
dfa38cf4cd4batagen2026-10-10 09:17 UTC489
    pub fn choose(&mut self, choice: &str) -> Result<Outcome, Problem> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC490
        if !self.branch_pending {
dfa38cf4cd4batagen2026-10-10 09:17 UTC491
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC492
                StatusCode::CONFLICT,
dfa38cf4cd4batagen2026-10-10 09:17 UTC493
                "no-decision-pending",
dfa38cf4cd4batagen2026-10-10 09:17 UTC494
                "There is no decision pending.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC495
            ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC496
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC497
        let (location, detail) = match choice {
dfa38cf4cd4batagen2026-10-10 09:17 UTC498
            "forum" => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC499
                self.forum_read = true;
dfa38cf4cd4batagen2026-10-10 09:17 UTC500
                self.shame += 3;
dfa38cf4cd4batagen2026-10-10 09:17 UTC501
                (
dfa38cf4cd4batagen2026-10-10 09:17 UTC502
                    format!("/runs/{}/techniques", self.id),
dfa38cf4cd4batagen2026-10-10 09:17 UTC503
                    "The thread is from 2009. A technique called uli is discussed at length.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC504
                )
dfa38cf4cd4batagen2026-10-10 09:17 UTC505
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC506
            "close-tab" => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC507
                self.suspicion = 0;
dfa38cf4cd4batagen2026-10-10 09:17 UTC508
                (
dfa38cf4cd4batagen2026-10-10 09:17 UTC509
                    format!("/runs/{}", self.id),
dfa38cf4cd4batagen2026-10-10 09:17 UTC510
                    "The tab is closed. Nobody saw it.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC511
                )
dfa38cf4cd4batagen2026-10-10 09:17 UTC512
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC513
            _ => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC514
                return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC515
                    StatusCode::NOT_FOUND,
dfa38cf4cd4batagen2026-10-10 09:17 UTC516
                    "no-such-choice",
dfa38cf4cd4batagen2026-10-10 09:17 UTC517
                    "That was not one of the choices.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC518
                ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC519
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC520
        };
dfa38cf4cd4batagen2026-10-10 09:17 UTC521
        self.branch_pending = false;
dfa38cf4cd4batagen2026-10-10 09:17 UTC522
        self.touch();
dfa38cf4cd4batagen2026-10-10 09:17 UTC523
        Ok(
dfa38cf4cd4batagen2026-10-10 09:17 UTC524
            Outcome::json(StatusCode::SEE_OTHER, json!({ "detail": detail }))
dfa38cf4cd4batagen2026-10-10 09:17 UTC525
                .header(header::LOCATION, location),
dfa38cf4cd4batagen2026-10-10 09:17 UTC526
        )
dfa38cf4cd4batagen2026-10-10 09:17 UTC527
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC528
dfa38cf4cd4batagen2026-10-10 09:17 UTC529
    pub fn put(&mut self) -> Outcome {
dfa38cf4cd4batagen2026-10-10 09:17 UTC530
        if self.put_applied {
dfa38cf4cd4batagen2026-10-10 09:17 UTC531
            return Outcome::empty(StatusCode::NO_CONTENT);
dfa38cf4cd4batagen2026-10-10 09:17 UTC532
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC533
        self.put_applied = true;
dfa38cf4cd4batagen2026-10-10 09:17 UTC534
        self.length += 0.01;
dfa38cf4cd4batagen2026-10-10 09:17 UTC535
        self.touch();
dfa38cf4cd4batagen2026-10-10 09:17 UTC536
        Outcome::json(StatusCode::OK, json!({ "detail": "Applied." }))
dfa38cf4cd4batagen2026-10-10 09:17 UTC537
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC538
dfa38cf4cd4batagen2026-10-10 09:17 UTC539
    pub fn patch(&mut self, ops: &[PatchOp]) -> Result<Outcome, Problem> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC540
        for op in ops {
dfa38cf4cd4batagen2026-10-10 09:17 UTC541
            if op.op != "test" {
dfa38cf4cd4batagen2026-10-10 09:17 UTC542
                return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC543
                    StatusCode::UNPROCESSABLE_ENTITY,
dfa38cf4cd4batagen2026-10-10 09:17 UTC544
                    "wishful-thinking",
dfa38cf4cd4batagen2026-10-10 09:17 UTC545
                    "The server accepts only the truth.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC546
                )
dfa38cf4cd4batagen2026-10-10 09:17 UTC547
                .with("op", op.op.clone()));
dfa38cf4cd4batagen2026-10-10 09:17 UTC548
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC549
            let actual = match op.path.as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC550
                "/length" => self.current_length(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC551
                "/girth" => self.girth,
dfa38cf4cd4batagen2026-10-10 09:17 UTC552
                "/foreskin" => self.foreskin.unwrap_or(0.0),
dfa38cf4cd4batagen2026-10-10 09:17 UTC553
                _ => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC554
                    return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC555
                        StatusCode::UNPROCESSABLE_ENTITY,
dfa38cf4cd4batagen2026-10-10 09:17 UTC556
                        "no-such-measurement",
dfa38cf4cd4batagen2026-10-10 09:17 UTC557
                        "There is no such measurement.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC558
                    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC559
                    .with("path", op.path.clone()));
dfa38cf4cd4batagen2026-10-10 09:17 UTC560
                }
dfa38cf4cd4batagen2026-10-10 09:17 UTC561
            };
dfa38cf4cd4batagen2026-10-10 09:17 UTC562
            let Some(claimed) = op.value.as_f64() else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC563
                return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC564
                    StatusCode::BAD_REQUEST,
dfa38cf4cd4batagen2026-10-10 09:17 UTC565
                    "malformed-patch",
dfa38cf4cd4batagen2026-10-10 09:17 UTC566
                    "A test requires a numeric value.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC567
                ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC568
            };
dfa38cf4cd4batagen2026-10-10 09:17 UTC569
            if op.path == "/length" && claimed >= actual + 1.0 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC570
                self.shame += 2;
dfa38cf4cd4batagen2026-10-10 09:17 UTC571
                self.touch();
dfa38cf4cd4batagen2026-10-10 09:17 UTC572
                return Err(exaggeration());
dfa38cf4cd4batagen2026-10-10 09:17 UTC573
            }
177535ac8abfatagen2026-10-10 10:39 UTC574
            if !honest(claimed, actual) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC575
                self.shame += 1;
dfa38cf4cd4batagen2026-10-10 09:17 UTC576
                self.touch();
dfa38cf4cd4batagen2026-10-10 09:17 UTC577
                return Err(
dfa38cf4cd4batagen2026-10-10 09:17 UTC578
                    Problem::new(StatusCode::CONFLICT, "test-failed", "The test failed.")
dfa38cf4cd4batagen2026-10-10 09:17 UTC579
                        .with("path", op.path.clone()),
dfa38cf4cd4batagen2026-10-10 09:17 UTC580
                );
dfa38cf4cd4batagen2026-10-10 09:17 UTC581
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC582
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC583
        Ok(Outcome::json(
dfa38cf4cd4batagen2026-10-10 09:17 UTC584
            StatusCode::OK,
dfa38cf4cd4batagen2026-10-10 09:17 UTC585
            json!({ "detail": "Correct." }),
dfa38cf4cd4batagen2026-10-10 09:17 UTC586
        ))
dfa38cf4cd4batagen2026-10-10 09:17 UTC587
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC588
dfa38cf4cd4batagen2026-10-10 09:17 UTC589
    pub fn delete_foreskin(&mut self, if_match: Option<&str>) -> Result<Outcome, Problem> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC590
        if self.foreskin.is_none() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC591
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC592
                StatusCode::GONE,
dfa38cf4cd4batagen2026-10-10 09:17 UTC593
                "foreskin-gone",
dfa38cf4cd4batagen2026-10-10 09:17 UTC594
                "The foreskin is gone. It will not be returning.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC595
            ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC596
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC597
        let Some(if_match) = if_match else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC598
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC599
                StatusCode::PRECONDITION_REQUIRED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC600
                "confirmation-required",
dfa38cf4cd4batagen2026-10-10 09:17 UTC601
                "The server requires you to be sure. Send If-Match.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC602
            ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC603
        };
dfa38cf4cd4batagen2026-10-10 09:17 UTC604
        if !super::etag_matches(if_match, &self.etag()) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC605
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC606
                StatusCode::PRECONDITION_FAILED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC607
                "stale-foreskin",
dfa38cf4cd4batagen2026-10-10 09:17 UTC608
                "The foreskin has changed since you last looked at it.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC609
            ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC610
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC611
        self.foreskin = None;
dfa38cf4cd4batagen2026-10-10 09:17 UTC612
        self.shame += 20;
dfa38cf4cd4batagen2026-10-10 09:17 UTC613
        self.touch();
dfa38cf4cd4batagen2026-10-10 09:17 UTC614
        Ok(Outcome::empty(StatusCode::NO_CONTENT))
dfa38cf4cd4batagen2026-10-10 09:17 UTC615
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC616
dfa38cf4cd4batagen2026-10-10 09:17 UTC617
    pub fn set_grip(&mut self, grip: Grip) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC618
        self.grip = grip;
dfa38cf4cd4batagen2026-10-10 09:17 UTC619
        self.touch();
dfa38cf4cd4batagen2026-10-10 09:17 UTC620
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC621
dfa38cf4cd4batagen2026-10-10 09:17 UTC622
    pub fn accept_terms(&mut self) -> Result<Outcome, Problem> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC623
        if self.floor != Floor::Hotel {
dfa38cf4cd4batagen2026-10-10 09:17 UTC624
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC625
                StatusCode::NOT_FOUND,
dfa38cf4cd4batagen2026-10-10 09:17 UTC626
                "no-portal",
dfa38cf4cd4batagen2026-10-10 09:17 UTC627
                "There is no captive portal here. There is only the regular kind of captivity.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC628
            ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC629
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC630
        self.wifi_accepted = true;
dfa38cf4cd4batagen2026-10-10 09:17 UTC631
        self.touch();
dfa38cf4cd4batagen2026-10-10 09:17 UTC632
        Ok(Outcome::json(
dfa38cf4cd4batagen2026-10-10 09:17 UTC633
            StatusCode::SEE_OTHER,
dfa38cf4cd4batagen2026-10-10 09:17 UTC634
            json!({ "detail": "Connected. The hotel does not want to know." }),
dfa38cf4cd4batagen2026-10-10 09:17 UTC635
        )
dfa38cf4cd4batagen2026-10-10 09:17 UTC636
        .header(header::LOCATION, format!("/runs/{}", self.id)))
dfa38cf4cd4batagen2026-10-10 09:17 UTC637
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC638
dfa38cf4cd4batagen2026-10-10 09:17 UTC639
    pub fn elder(
dfa38cf4cd4batagen2026-10-10 09:17 UTC640
        &mut self,
dfa38cf4cd4batagen2026-10-10 09:17 UTC641
        proxy_authorization: Option<&str>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC642
        if_modified_since: Option<&str>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC643
    ) -> Result<Outcome, Problem> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC644
        if self.floor != Floor::Forum {
dfa38cf4cd4batagen2026-10-10 09:17 UTC645
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC646
                StatusCode::NOT_FOUND,
dfa38cf4cd4batagen2026-10-10 09:17 UTC647
                "elder-absent",
dfa38cf4cd4batagen2026-10-10 09:17 UTC648
                "The Elder only posts on the forum.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC649
            ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC650
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC651
        if !self.verified {
dfa38cf4cd4batagen2026-10-10 09:17 UTC652
            let verification = |detail: &str| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC653
                Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC654
                    StatusCode::PROXY_AUTHENTICATION_REQUIRED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC655
                    "verification-required",
dfa38cf4cd4batagen2026-10-10 09:17 UTC656
                    detail,
dfa38cf4cd4batagen2026-10-10 09:17 UTC657
                )
dfa38cf4cd4batagen2026-10-10 09:17 UTC658
                .header(header::PROXY_AUTHENTICATE, "Ruler realm=\"forum\"")
dfa38cf4cd4batagen2026-10-10 09:17 UTC659
            };
dfa38cf4cd4batagen2026-10-10 09:17 UTC660
            let Some(credentials) = proxy_authorization else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC661
                return Err(verification("The forum requires verification."));
dfa38cf4cd4batagen2026-10-10 09:17 UTC662
            };
dfa38cf4cd4batagen2026-10-10 09:17 UTC663
            let Some(claimed) =
dfa38cf4cd4batagen2026-10-10 09:17 UTC664
                parse_credentials(credentials, "ruler").and_then(|v| v.parse::<f64>().ok())
dfa38cf4cd4batagen2026-10-10 09:17 UTC665
            else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC666
                return Err(verification("The forum requires verification, in inches."));
dfa38cf4cd4batagen2026-10-10 09:17 UTC667
            };
dfa38cf4cd4batagen2026-10-10 09:17 UTC668
            let actual = self.current_length();
dfa38cf4cd4batagen2026-10-10 09:17 UTC669
            if claimed >= actual + 1.0 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC670
                self.shame += 2;
99524cf9f17catagen2026-10-10 10:53 UTC671
                return Err(self.elder_strike(exaggeration()));
dfa38cf4cd4batagen2026-10-10 09:17 UTC672
            }
177535ac8abfatagen2026-10-10 10:39 UTC673
            if !honest(claimed, actual) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC674
                self.shame += 1;
99524cf9f17catagen2026-10-10 10:53 UTC675
                return Err(
99524cf9f17catagen2026-10-10 10:53 UTC676
                    self.elder_strike(verification("Verification failed. The forum can tell."))
99524cf9f17catagen2026-10-10 10:53 UTC677
                );
dfa38cf4cd4batagen2026-10-10 09:17 UTC678
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC679
            self.verified = true;
dfa38cf4cd4batagen2026-10-10 09:17 UTC680
            self.touch();
dfa38cf4cd4batagen2026-10-10 09:17 UTC681
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC682
dfa38cf4cd4batagen2026-10-10 09:17 UTC683
        let last_modified =
dfa38cf4cd4batagen2026-10-10 09:17 UTC684
            httpdate::parse_http_date(ELDER_LAST_MODIFIED).expect("valid constant date");
dfa38cf4cd4batagen2026-10-10 09:17 UTC685
        let current = if_modified_since
dfa38cf4cd4batagen2026-10-10 09:17 UTC686
            .and_then(|d| httpdate::parse_http_date(d).ok())
dfa38cf4cd4batagen2026-10-10 09:17 UTC687
            .is_some_and(|since| since >= last_modified);
dfa38cf4cd4batagen2026-10-10 09:17 UTC688
        if current {
dfa38cf4cd4batagen2026-10-10 09:17 UTC689
            self.elder_beaten = true;
dfa38cf4cd4batagen2026-10-10 09:17 UTC690
            self.touch();
dfa38cf4cd4batagen2026-10-10 09:17 UTC691
            return Ok(Outcome::empty(StatusCode::NOT_MODIFIED)
dfa38cf4cd4batagen2026-10-10 09:17 UTC692
                .header(header::LAST_MODIFIED, ELDER_LAST_MODIFIED));
dfa38cf4cd4batagen2026-10-10 09:17 UTC693
        }
c3e0c27cf8fdatagen2026-10-10 11:28 UTC694
        let spreadsheet = Outcome::raw(StatusCode::OK, "text/csv; header=present", SPREADSHEET)
c3e0c27cf8fdatagen2026-10-10 11:28 UTC695
            .header(header::LAST_MODIFIED, ELDER_LAST_MODIFIED);
c3e0c27cf8fdatagen2026-10-10 11:28 UTC696
        if self.filmed_at.is_some() {
c3e0c27cf8fdatagen2026-10-10 11:28 UTC697
            return Ok(spreadsheet.header(
c3e0c27cf8fdatagen2026-10-10 11:28 UTC698
                HeaderName::from_static("x-forum-note"),
c3e0c27cf8fdatagen2026-10-10 11:28 UTC699
                "We have seen the footage.",
c3e0c27cf8fdatagen2026-10-10 11:28 UTC700
            ));
c3e0c27cf8fdatagen2026-10-10 11:28 UTC701
        }
c3e0c27cf8fdatagen2026-10-10 11:28 UTC702
        Ok(spreadsheet)
dfa38cf4cd4batagen2026-10-10 09:17 UTC703
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC704
dfa38cf4cd4batagen2026-10-10 09:17 UTC705
    pub fn diagnose(
dfa38cf4cd4batagen2026-10-10 09:17 UTC706
        &mut self,
dfa38cf4cd4batagen2026-10-10 09:17 UTC707
        authorization: Option<&str>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC708
        if_none_match: Option<&str>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC709
    ) -> Result<Outcome, Problem> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC710
        if self.floor != Floor::Clinic {
dfa38cf4cd4batagen2026-10-10 09:17 UTC711
            let floors = Floor::Clinic.number() - self.floor.number();
dfa38cf4cd4batagen2026-10-10 09:17 UTC712
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC713
                StatusCode::TOO_EARLY,
dfa38cf4cd4batagen2026-10-10 09:17 UTC714
                "no-appointment",
dfa38cf4cd4batagen2026-10-10 09:17 UTC715
                format!("The clinic's next available appointment is {floors} floors away."),
dfa38cf4cd4batagen2026-10-10 09:17 UTC716
            ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC717
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC718
        let intake = Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC719
            StatusCode::UNAUTHORIZED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC720
            "intake",
dfa38cf4cd4batagen2026-10-10 09:17 UTC721
            "The clinic requires authorization.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC722
        );
dfa38cf4cd4batagen2026-10-10 09:17 UTC723
        let Some(authorization) = authorization else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC724
            return Err(intake.header(header::WWW_AUTHENTICATE, "Bearer realm=\"clinic\""));
dfa38cf4cd4batagen2026-10-10 09:17 UTC725
        };
177535ac8abfatagen2026-10-10 10:39 UTC726
        let truthful = parse_credentials(authorization, "bearer")
dfa38cf4cd4batagen2026-10-10 09:17 UTC727
            .is_some_and(|token| token.eq_ignore_ascii_case("honesty"));
177535ac8abfatagen2026-10-10 10:39 UTC728
        if !truthful {
dfa38cf4cd4batagen2026-10-10 09:17 UTC729
            let hint = INTAKE[self.intake_attempts.min(INTAKE.len() - 1)];
dfa38cf4cd4batagen2026-10-10 09:17 UTC730
            self.intake_attempts += 1;
dfa38cf4cd4batagen2026-10-10 09:17 UTC731
            return Err(intake.header(
dfa38cf4cd4batagen2026-10-10 09:17 UTC732
                header::WWW_AUTHENTICATE,
dfa38cf4cd4batagen2026-10-10 09:17 UTC733
                format!(
dfa38cf4cd4batagen2026-10-10 09:17 UTC734
                    "Bearer realm=\"clinic\", error=\"invalid_token\", error_description=\"{hint}\""
dfa38cf4cd4batagen2026-10-10 09:17 UTC735
                ),
dfa38cf4cd4batagen2026-10-10 09:17 UTC736
            ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC737
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC738
dfa38cf4cd4batagen2026-10-10 09:17 UTC739
        self.peyronie = false;
c3e0c27cf8fdatagen2026-10-10 11:28 UTC740
        self.priapism_turns = 0;
dfa38cf4cd4batagen2026-10-10 09:17 UTC741
        let etag = self.origin_etag.clone();
dfa38cf4cd4batagen2026-10-10 09:17 UTC742
        if if_none_match.is_some_and(|inm| super::etag_matches(inm, &etag)) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC743
            self.cured = true;
dfa38cf4cd4batagen2026-10-10 09:17 UTC744
            return Ok(Outcome::empty(StatusCode::NOT_MODIFIED)
dfa38cf4cd4batagen2026-10-10 09:17 UTC745
                .header(header::ETAG, etag)
dfa38cf4cd4batagen2026-10-10 09:17 UTC746
                .header(X_DIAGNOSIS, "normal"));
dfa38cf4cd4batagen2026-10-10 09:17 UTC747
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC748
        if !self.measured {
dfa38cf4cd4batagen2026-10-10 09:17 UTC749
            self.measured = true;
dfa38cf4cd4batagen2026-10-10 09:17 UTC750
            if !self.items.ruler {
dfa38cf4cd4batagen2026-10-10 09:17 UTC751
                self.shame += 15;
dfa38cf4cd4batagen2026-10-10 09:17 UTC752
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC753
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC754
        Ok(Outcome::json(
dfa38cf4cd4batagen2026-10-10 09:17 UTC755
            StatusCode::OK,
dfa38cf4cd4batagen2026-10-10 09:17 UTC756
            json!({
dfa38cf4cd4batagen2026-10-10 09:17 UTC757
                "diagnosis": "within normal range",
dfa38cf4cd4batagen2026-10-10 09:17 UTC758
                "measured_length": round2(self.length),
dfa38cf4cd4batagen2026-10-10 09:17 UTC759
                "you_said": round2(self.self_reported()),
dfa38cf4cd4batagen2026-10-10 09:17 UTC760
                "prescription": "Please stop.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC761
            }),
dfa38cf4cd4batagen2026-10-10 09:17 UTC762
        )
dfa38cf4cd4batagen2026-10-10 09:17 UTC763
        .header(header::ETAG, etag))
dfa38cf4cd4batagen2026-10-10 09:17 UTC764
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC765
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC766
dfa38cf4cd4batagen2026-10-10 09:17 UTC767
fn pump_bro_absent() -> Problem {
dfa38cf4cd4batagen2026-10-10 09:17 UTC768
    Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC769
        StatusCode::NOT_FOUND,
dfa38cf4cd4batagen2026-10-10 09:17 UTC770
        "pump-bro-absent",
dfa38cf4cd4batagen2026-10-10 09:17 UTC771
        "Pump-Bro is not here.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC772
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC773
}