atagen

jelq

public

an http based roguelike rpg about jelqing

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

Blame: src/game/turn.rs

a6caa2f99786atagen2026-10-12 10:38 UTC1
use std::{fmt::Write as _, ops::ControlFlow};
782069860236atagen2026-10-10 12:44 UTC2
782069860236atagen2026-10-10 12:44 UTC3
use axum::http::{StatusCode, header};
782069860236atagen2026-10-10 12:44 UTC4
use serde_json::json;
782069860236atagen2026-10-10 12:44 UTC5
782069860236atagen2026-10-10 12:44 UTC6
use super::{
782069860236atagen2026-10-10 12:44 UTC7
    Floor, Grip, LUBRICANTS, Lube, Outcome, Payload, Run, Story, Technique,
782069860236atagen2026-10-10 12:44 UTC8
    balance::{
b85a0d4536c3atagen2026-10-11 08:50 UTC9
        ARRIVAL_BONUS, BAND_BONUS, BRANCH_TURN, CAT_TAKES_TOWEL, CHANGING_ROOM_CHILL, DANGER_EQ,
058650ec79d6atagen2026-10-12 10:49 UTC10
        EQ_BAND, EQ_WOBBLE, EXTENDER_GAIN, FORESKIN_GLIDE, FRICTION_BURN, FRICTION_BURN_AT,
058650ec79d6atagen2026-10-12 10:49 UTC11
        FRICTION_DRY, FRICTION_GRIP, FRICTION_HOLD, FRICTION_MAX, FRICTION_MEDITATE, FRICTION_SPIT,
058650ec79d6atagen2026-10-12 10:49 UTC12
        GIRTH_THICK, GREED_HOT, GREED_WARM, HOLD_EQ, HOT_EQ, MEDITATION_EQ, OBSERVED, PRIAPISM_EQ,
058650ec79d6atagen2026-10-12 10:49 UTC13
        PRIAPISM_TURNS, PULLED_MUSCLE, PULLED_MUSCLE_INTENSITY, REFLECTION, RING_PRIAPISM,
9374b3933189atagen2026-10-12 11:15 UTC14
        RING_RUPTURE_LINE, RING_TIGHT_PRIAPISM, RUPTURE_HOLD, RUPTURE_LINE, STORAGE_BULK,
9374b3933189atagen2026-10-12 11:15 UTC15
        SUSPICION_LIMIT,
782069860236atagen2026-10-10 12:44 UTC16
    },
782069860236atagen2026-10-10 12:44 UTC17
    not_extended, round3,
9683736a2c5eatagen2026-10-10 13:15 UTC18
    state::{MumVisit, Observer, Pending},
782069860236atagen2026-10-10 12:44 UTC19
    techniques::{LubeRule, Profile, Training, Utility},
782069860236atagen2026-10-10 12:44 UTC20
};
782069860236atagen2026-10-10 12:44 UTC21
use crate::{headers::SERVER_TIMING, problem::Problem};
782069860236atagen2026-10-10 12:44 UTC22
782069860236atagen2026-10-10 12:44 UTC23
impl Run {
782069860236atagen2026-10-10 12:44 UTC24
    pub fn jelq(
782069860236atagen2026-10-10 12:44 UTC25
        &mut self,
782069860236atagen2026-10-10 12:44 UTC26
        techniques: &[Technique],
782069860236atagen2026-10-10 12:44 UTC27
        intensity: i32,
782069860236atagen2026-10-10 12:44 UTC28
        lube: Option<Lube>,
782069860236atagen2026-10-10 12:44 UTC29
    ) -> Result<Outcome, Problem> {
a6caa2f99786atagen2026-10-12 10:38 UTC30
        self.check_exhausted()?;
782069860236atagen2026-10-10 12:44 UTC31
        if self.story == Story::Pending {
782069860236atagen2026-10-10 12:44 UTC32
            return Ok(self.branch_offer());
782069860236atagen2026-10-10 12:44 UTC33
        }
782069860236atagen2026-10-10 12:44 UTC34
        if self.floor == Floor::Forum {
782069860236atagen2026-10-10 12:44 UTC35
            return Ok(Outcome::json(
782069860236atagen2026-10-10 12:44 UTC36
                StatusCode::SEE_OTHER,
782069860236atagen2026-10-10 12:44 UTC37
                json!({ "detail": "Refer to the forum." }),
782069860236atagen2026-10-10 12:44 UTC38
            )
782069860236atagen2026-10-10 12:44 UTC39
            .header(header::LOCATION, super::FORUM_THREAD));
782069860236atagen2026-10-10 12:44 UTC40
        }
782069860236atagen2026-10-10 12:44 UTC41
        if self.items.cage {
782069860236atagen2026-10-10 12:44 UTC42
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC43
                StatusCode::LOCKED,
782069860236atagen2026-10-10 12:44 UTC44
                "caged",
782069860236atagen2026-10-10 12:44 UTC45
                "The cage is locked. The key is not in the box.",
782069860236atagen2026-10-10 12:44 UTC46
            ));
782069860236atagen2026-10-10 12:44 UTC47
        }
782069860236atagen2026-10-10 12:44 UTC48
        match *techniques {
a6caa2f99786atagen2026-10-12 10:38 UTC49
            [Technique::Utility(utility)] => self.utility(utility, lube),
a6caa2f99786atagen2026-10-12 10:38 UTC50
            [Technique::Training(training)] => self.single(training, intensity, lube),
a6caa2f99786atagen2026-10-12 10:38 UTC51
            [Technique::Training(a), Technique::Training(b)] => self.combo(a, b, intensity, lube),
782069860236atagen2026-10-10 12:44 UTC52
            [_, _] => Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC53
                StatusCode::BAD_REQUEST,
782069860236atagen2026-10-10 12:44 UTC54
                "combo-invalid",
782069860236atagen2026-10-10 12:44 UTC55
                "Only training techniques can be combined.",
782069860236atagen2026-10-10 12:44 UTC56
            )),
782069860236atagen2026-10-10 12:44 UTC57
            _ => Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC58
                StatusCode::BAD_REQUEST,
782069860236atagen2026-10-10 12:44 UTC59
                "too-many-techniques",
782069860236atagen2026-10-10 12:44 UTC60
                "Two techniques at most. The server has limits even if you do not.",
782069860236atagen2026-10-10 12:44 UTC61
            )),
782069860236atagen2026-10-10 12:44 UTC62
        }
782069860236atagen2026-10-10 12:44 UTC63
    }
782069860236atagen2026-10-10 12:44 UTC64
a6caa2f99786atagen2026-10-12 10:38 UTC65
    fn utility(&mut self, utility: Utility, lube: Option<Lube>) -> Result<Outcome, Problem> {
782069860236atagen2026-10-10 12:44 UTC66
        match utility {
782069860236atagen2026-10-10 12:44 UTC67
            Utility::Warmup => return self.warm_up(),
782069860236atagen2026-10-10 12:44 UTC68
            Utility::Ice => return self.ice(),
782069860236atagen2026-10-10 12:44 UTC69
            Utility::Supplement => return self.supplement(),
0fdc47364c8catagen2026-10-11 04:45 UTC70
            Utility::Meditate => return self.meditate(),
0a0bfee8329eatagen2026-10-11 05:11 UTC71
            Utility::Energy => return self.energy(),
782069860236atagen2026-10-10 12:44 UTC72
            Utility::Edge | Utility::Pump => {}
782069860236atagen2026-10-10 12:44 UTC73
        }
782069860236atagen2026-10-10 12:44 UTC74
        self.ready(Technique::Utility(utility), lube)?;
782069860236atagen2026-10-10 12:44 UTC75
        self.check_storage()?;
782069860236atagen2026-10-10 12:44 UTC76
        self.floor_enemy()?;
782069860236atagen2026-10-10 12:44 UTC77
        match utility {
a6caa2f99786atagen2026-10-12 10:38 UTC78
            Utility::Edge => self.edge(),
a6caa2f99786atagen2026-10-12 10:38 UTC79
            _ => self.pump(),
782069860236atagen2026-10-10 12:44 UTC80
        }
782069860236atagen2026-10-10 12:44 UTC81
    }
782069860236atagen2026-10-10 12:44 UTC82
782069860236atagen2026-10-10 12:44 UTC83
    fn single(
782069860236atagen2026-10-10 12:44 UTC84
        &mut self,
782069860236atagen2026-10-10 12:44 UTC85
        training: Training,
782069860236atagen2026-10-10 12:44 UTC86
        intensity: i32,
782069860236atagen2026-10-10 12:44 UTC87
        lube: Option<Lube>,
782069860236atagen2026-10-10 12:44 UTC88
    ) -> Result<Outcome, Problem> {
782069860236atagen2026-10-10 12:44 UTC89
        self.ready(Technique::Training(training), lube)?;
782069860236atagen2026-10-10 12:44 UTC90
        self.check_storage()?;
14de3e1e8dc2atagen2026-10-11 06:19 UTC91
        self.lube_available(lube)?;
782069860236atagen2026-10-10 12:44 UTC92
        self.floor_enemy()?;
139164489032atagen2026-10-11 05:46 UTC93
        self.take_lube(lube)?;
a6caa2f99786atagen2026-10-12 10:38 UTC94
        self.chafe(lube, intensity);
a6caa2f99786atagen2026-10-12 10:38 UTC95
        self.commit(intensity, lube, 1);
782069860236atagen2026-10-10 12:44 UTC96
        if let ControlFlow::Break(interrupted) = self.mishaps(training, intensity)? {
782069860236atagen2026-10-10 12:44 UTC97
            return Ok(interrupted);
782069860236atagen2026-10-10 12:44 UTC98
        }
782069860236atagen2026-10-10 12:44 UTC99
        if let ControlFlow::Break(interrupted) = self.hazards(intensity)? {
782069860236atagen2026-10-10 12:44 UTC100
            return Ok(interrupted);
782069860236atagen2026-10-10 12:44 UTC101
        }
782069860236atagen2026-10-10 12:44 UTC102
        let outcome = self.roll(training, intensity, lube)?;
782069860236atagen2026-10-10 12:44 UTC103
        Ok(self.gossip(outcome))
782069860236atagen2026-10-10 12:44 UTC104
    }
782069860236atagen2026-10-10 12:44 UTC105
782069860236atagen2026-10-10 12:44 UTC106
    fn combo(
782069860236atagen2026-10-10 12:44 UTC107
        &mut self,
782069860236atagen2026-10-10 12:44 UTC108
        a: Training,
782069860236atagen2026-10-10 12:44 UTC109
        b: Training,
782069860236atagen2026-10-10 12:44 UTC110
        intensity: i32,
782069860236atagen2026-10-10 12:44 UTC111
        lube: Option<Lube>,
782069860236atagen2026-10-10 12:44 UTC112
    ) -> Result<Outcome, Problem> {
782069860236atagen2026-10-10 12:44 UTC113
        if a == b {
782069860236atagen2026-10-10 12:44 UTC114
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC115
                StatusCode::BAD_REQUEST,
782069860236atagen2026-10-10 12:44 UTC116
                "combo-invalid",
782069860236atagen2026-10-10 12:44 UTC117
                "A technique cannot be combined with itself. The server has checked.",
782069860236atagen2026-10-10 12:44 UTC118
            ));
782069860236atagen2026-10-10 12:44 UTC119
        }
782069860236atagen2026-10-10 12:44 UTC120
        self.ready(Technique::Training(a), lube)?;
782069860236atagen2026-10-10 12:44 UTC121
        self.ready(Technique::Training(b), lube)?;
782069860236atagen2026-10-10 12:44 UTC122
        self.check_storage()?;
14de3e1e8dc2atagen2026-10-11 06:19 UTC123
        self.lube_available(lube)?;
782069860236atagen2026-10-10 12:44 UTC124
        self.floor_enemy()?;
139164489032atagen2026-10-11 05:46 UTC125
        self.take_lube(lube)?;
a6caa2f99786atagen2026-10-12 10:38 UTC126
        self.chafe(lube, intensity);
c4d70621d377atagen2026-10-12 11:56 UTC127
        self.chafe(lube, intensity);
a6caa2f99786atagen2026-10-12 10:38 UTC128
        self.commit(intensity, lube, 2);
782069860236atagen2026-10-10 12:44 UTC129
        for training in [a, b] {
782069860236atagen2026-10-10 12:44 UTC130
            if let ControlFlow::Break(interrupted) = self.mishaps(training, intensity)? {
782069860236atagen2026-10-10 12:44 UTC131
                return Ok(interrupted);
782069860236atagen2026-10-10 12:44 UTC132
            }
782069860236atagen2026-10-10 12:44 UTC133
        }
782069860236atagen2026-10-10 12:44 UTC134
        if let ControlFlow::Break(interrupted) = self.hazards(intensity)? {
782069860236atagen2026-10-10 12:44 UTC135
            return Ok(interrupted);
782069860236atagen2026-10-10 12:44 UTC136
        }
782069860236atagen2026-10-10 12:44 UTC137
782069860236atagen2026-10-10 12:44 UTC138
        let mut statuses = Vec::new();
782069860236atagen2026-10-10 12:44 UTC139
        for training in [a, b] {
782069860236atagen2026-10-10 12:44 UTC140
            let result = self.roll(training, intensity, lube);
782069860236atagen2026-10-10 12:44 UTC141
            if self.ended.is_some() {
782069860236atagen2026-10-10 12:44 UTC142
                return result;
782069860236atagen2026-10-10 12:44 UTC143
            }
782069860236atagen2026-10-10 12:44 UTC144
            let status = match result {
782069860236atagen2026-10-10 12:44 UTC145
                Ok(outcome) => self.gossip(outcome).status,
782069860236atagen2026-10-10 12:44 UTC146
                Err(problem) => problem.status(),
782069860236atagen2026-10-10 12:44 UTC147
            };
782069860236atagen2026-10-10 12:44 UTC148
            statuses.push((training, status));
782069860236atagen2026-10-10 12:44 UTC149
        }
782069860236atagen2026-10-10 12:44 UTC150
782069860236atagen2026-10-10 12:44 UTC151
        let mut xml = String::from(
782069860236atagen2026-10-10 12:44 UTC152
            "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<D:multistatus xmlns:D=\"DAV:\">\n",
782069860236atagen2026-10-10 12:44 UTC153
        );
782069860236atagen2026-10-10 12:44 UTC154
        for (training, status) in statuses {
782069860236atagen2026-10-10 12:44 UTC155
            let _ = write!(
782069860236atagen2026-10-10 12:44 UTC156
                xml,
782069860236atagen2026-10-10 12:44 UTC157
                "  <D:response>\n    <D:href>{}?technique={}</D:href>\n    <D:status>HTTP/1.1 {} {}</D:status>\n  </D:response>\n",
782069860236atagen2026-10-10 12:44 UTC158
                self.path(""),
782069860236atagen2026-10-10 12:44 UTC159
                training.name(),
782069860236atagen2026-10-10 12:44 UTC160
                status.as_u16(),
782069860236atagen2026-10-10 12:44 UTC161
                crate::problem::reason(status),
782069860236atagen2026-10-10 12:44 UTC162
            );
782069860236atagen2026-10-10 12:44 UTC163
        }
782069860236atagen2026-10-10 12:44 UTC164
        xml.push_str(
782069860236atagen2026-10-10 12:44 UTC165
            "  <D:responsedescription>Each technique has been reported separately.</D:responsedescription>\n</D:multistatus>\n",
782069860236atagen2026-10-10 12:44 UTC166
        );
782069860236atagen2026-10-10 12:44 UTC167
        Ok(Outcome::raw(
782069860236atagen2026-10-10 12:44 UTC168
            StatusCode::MULTI_STATUS,
782069860236atagen2026-10-10 12:44 UTC169
            "application/xml; charset=utf-8",
782069860236atagen2026-10-10 12:44 UTC170
            xml,
782069860236atagen2026-10-10 12:44 UTC171
        ))
782069860236atagen2026-10-10 12:44 UTC172
    }
782069860236atagen2026-10-10 12:44 UTC173
782069860236atagen2026-10-10 12:44 UTC174
    fn ready(&self, technique: Technique, lube: Option<Lube>) -> Result<(), Problem> {
58e1fb3943e6atagen2026-10-10 13:30 UTC175
        self.access(technique)?;
58e1fb3943e6atagen2026-10-10 13:30 UTC176
        match technique {
58e1fb3943e6atagen2026-10-10 13:30 UTC177
            Technique::Training(training) => Self::check_lube(training, lube),
58e1fb3943e6atagen2026-10-10 13:30 UTC178
            Technique::Utility(_) => Ok(()),
58e1fb3943e6atagen2026-10-10 13:30 UTC179
        }
58e1fb3943e6atagen2026-10-10 13:30 UTC180
    }
58e1fb3943e6atagen2026-10-10 13:30 UTC181
58e1fb3943e6atagen2026-10-10 13:30 UTC182
    /// Whether the run could attempt the technique now, ignoring the lubricant.
58e1fb3943e6atagen2026-10-10 13:30 UTC183
    fn access(&self, technique: Technique) -> Result<(), Problem> {
b85a0d4536c3atagen2026-10-11 08:50 UTC184
        if technique == Technique::Training(Training::Uli) && !self.uli_taught {
782069860236atagen2026-10-10 12:44 UTC185
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC186
                StatusCode::FORBIDDEN,
782069860236atagen2026-10-10 12:44 UTC187
                "technique-locked",
b85a0d4536c3atagen2026-10-11 08:50 UTC188
                "uli is discussed on the forum, in a thread from 2009, by a man who never posted again. You have not found the reply.",
782069860236atagen2026-10-10 12:44 UTC189
            ));
782069860236atagen2026-10-10 12:44 UTC190
        }
782069860236atagen2026-10-10 12:44 UTC191
        if !self.warmed_up {
782069860236atagen2026-10-10 12:44 UTC192
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC193
                StatusCode::PRECONDITION_FAILED,
782069860236atagen2026-10-10 12:44 UTC194
                "cold-start",
782069860236atagen2026-10-10 12:44 UTC195
                "Warm-up was skipped. The server has noted this.",
782069860236atagen2026-10-10 12:44 UTC196
            )
782069860236atagen2026-10-10 12:44 UTC197
            .with("technique", Utility::Warmup.name()));
782069860236atagen2026-10-10 12:44 UTC198
        }
058650ec79d6atagen2026-10-12 10:49 UTC199
        if matches!(technique, Technique::Training(_)) {
058650ec79d6atagen2026-10-12 10:49 UTC200
            self.grip_closes(self.grip)?;
058650ec79d6atagen2026-10-12 10:49 UTC201
        }
58e1fb3943e6atagen2026-10-10 13:30 UTC202
        match technique {
58e1fb3943e6atagen2026-10-10 13:30 UTC203
            Technique::Utility(Utility::Pump) if !self.items.pump_pro => Err(Problem::new(
58e1fb3943e6atagen2026-10-10 13:30 UTC204
                StatusCode::FAILED_DEPENDENCY,
58e1fb3943e6atagen2026-10-10 13:30 UTC205
                "no-pump",
58e1fb3943e6atagen2026-10-10 13:30 UTC206
                "The pump technique depends on a pump.",
58e1fb3943e6atagen2026-10-10 13:30 UTC207
            )),
58e1fb3943e6atagen2026-10-10 13:30 UTC208
            Technique::Utility(_) => Ok(()),
58e1fb3943e6atagen2026-10-10 13:30 UTC209
            Technique::Training(training) => match training.profile().foreskin {
58e1fb3943e6atagen2026-10-10 13:30 UTC210
                Some((min, max)) => self.check_foreskin(min, max),
58e1fb3943e6atagen2026-10-10 13:30 UTC211
                None => Ok(()),
58e1fb3943e6atagen2026-10-10 13:30 UTC212
            },
782069860236atagen2026-10-10 12:44 UTC213
        }
58e1fb3943e6atagen2026-10-10 13:30 UTC214
    }
58e1fb3943e6atagen2026-10-10 13:30 UTC215
58e1fb3943e6atagen2026-10-10 13:30 UTC216
    fn check_lube(training: Training, lube: Option<Lube>) -> Result<(), Problem> {
58e1fb3943e6atagen2026-10-10 13:30 UTC217
        match (training.profile().lube, lube) {
58e1fb3943e6atagen2026-10-10 13:30 UTC218
            (LubeRule::Required, None) => Err(Problem::new(
58e1fb3943e6atagen2026-10-10 13:30 UTC219
                StatusCode::UNSUPPORTED_MEDIA_TYPE,
58e1fb3943e6atagen2026-10-10 13:30 UTC220
                "lubricant-required",
58e1fb3943e6atagen2026-10-10 13:30 UTC221
                format!(
58e1fb3943e6atagen2026-10-10 13:30 UTC222
                    "The {} technique requires a lubricant. None was declared.",
58e1fb3943e6atagen2026-10-10 13:30 UTC223
                    training.name()
58e1fb3943e6atagen2026-10-10 13:30 UTC224
                ),
58e1fb3943e6atagen2026-10-10 13:30 UTC225
            )
58e1fb3943e6atagen2026-10-10 13:30 UTC226
            .with("lubricants", json!(LUBRICANTS))),
58e1fb3943e6atagen2026-10-10 13:30 UTC227
            (LubeRule::Forbidden, Some(_)) => Err(Problem::new(
58e1fb3943e6atagen2026-10-10 13:30 UTC228
                StatusCode::UNSUPPORTED_MEDIA_TYPE,
58e1fb3943e6atagen2026-10-10 13:30 UTC229
                "lubricant-declared",
58e1fb3943e6atagen2026-10-10 13:30 UTC230
                format!(
58e1fb3943e6atagen2026-10-10 13:30 UTC231
                    "The {} technique was requested. A lubricant was declared.",
58e1fb3943e6atagen2026-10-10 13:30 UTC232
                    training.name()
58e1fb3943e6atagen2026-10-10 13:30 UTC233
                ),
58e1fb3943e6atagen2026-10-10 13:30 UTC234
            )),
58e1fb3943e6atagen2026-10-10 13:30 UTC235
            _ => Ok(()),
58e1fb3943e6atagen2026-10-10 13:30 UTC236
        }
58e1fb3943e6atagen2026-10-10 13:30 UTC237
    }
58e1fb3943e6atagen2026-10-10 13:30 UTC238
0a0bfee8329eatagen2026-10-11 05:11 UTC239
    /// One unit of whatever was declared. Spit is free and the tap is always on.
14de3e1e8dc2atagen2026-10-11 06:19 UTC240
    fn lube_available(&self, lube: Option<Lube>) -> Result<(), Problem> {
0a0bfee8329eatagen2026-10-11 05:11 UTC241
        let (held, name) = match lube {
0a0bfee8329eatagen2026-10-11 05:11 UTC242
            None | Some(Lube::Spit) => return Ok(()),
14de3e1e8dc2atagen2026-10-11 06:19 UTC243
            Some(Lube::BabyOil) => (&self.items.baby_oil, "baby-oil"),
14de3e1e8dc2atagen2026-10-11 06:19 UTC244
            Some(Lube::CoconutOil) => (&self.items.coconut_oil, "coconut-oil"),
14de3e1e8dc2atagen2026-10-11 06:19 UTC245
            Some(Lube::Lotion) => (&self.items.lotion, "lotion"),
14de3e1e8dc2atagen2026-10-11 06:19 UTC246
            Some(Lube::Vaseline) => (&self.items.vaseline, "vaseline"),
0a0bfee8329eatagen2026-10-11 05:11 UTC247
        };
0a0bfee8329eatagen2026-10-11 05:11 UTC248
        if *held == 0 {
0a0bfee8329eatagen2026-10-11 05:11 UTC249
            return Err(Problem::new(
139164489032atagen2026-10-11 05:46 UTC250
                StatusCode::UNSUPPORTED_MEDIA_TYPE,
0a0bfee8329eatagen2026-10-11 05:11 UTC251
                "lubricant-exhausted",
0a0bfee8329eatagen2026-10-11 05:11 UTC252
                format!("{name} was declared, but the bottle is empty."),
0a0bfee8329eatagen2026-10-11 05:11 UTC253
            ));
0a0bfee8329eatagen2026-10-11 05:11 UTC254
        }
14de3e1e8dc2atagen2026-10-11 06:19 UTC255
        Ok(())
14de3e1e8dc2atagen2026-10-11 06:19 UTC256
    }
14de3e1e8dc2atagen2026-10-11 06:19 UTC257
14de3e1e8dc2atagen2026-10-11 06:19 UTC258
    fn take_lube(&mut self, lube: Option<Lube>) -> Result<(), Problem> {
14de3e1e8dc2atagen2026-10-11 06:19 UTC259
        self.lube_available(lube)?;
14de3e1e8dc2atagen2026-10-11 06:19 UTC260
        let held = match lube {
14de3e1e8dc2atagen2026-10-11 06:19 UTC261
            None | Some(Lube::Spit) => return Ok(()),
14de3e1e8dc2atagen2026-10-11 06:19 UTC262
            Some(Lube::BabyOil) => &mut self.items.baby_oil,
14de3e1e8dc2atagen2026-10-11 06:19 UTC263
            Some(Lube::CoconutOil) => &mut self.items.coconut_oil,
14de3e1e8dc2atagen2026-10-11 06:19 UTC264
            Some(Lube::Lotion) => &mut self.items.lotion,
14de3e1e8dc2atagen2026-10-11 06:19 UTC265
            Some(Lube::Vaseline) => &mut self.items.vaseline,
14de3e1e8dc2atagen2026-10-11 06:19 UTC266
        };
0a0bfee8329eatagen2026-10-11 05:11 UTC267
        *held -= 1;
0a0bfee8329eatagen2026-10-11 05:11 UTC268
        Ok(())
0a0bfee8329eatagen2026-10-11 05:11 UTC269
    }
0a0bfee8329eatagen2026-10-11 05:11 UTC270
58e1fb3943e6atagen2026-10-10 13:30 UTC271
    /// The techniques a JELQ could use right now without being refused before the roll.
a6caa2f99786atagen2026-10-12 10:38 UTC272
    pub fn techniques_now(&self) -> Vec<Technique> {
a6caa2f99786atagen2026-10-12 10:38 UTC273
        let blocked = !self.floor.timed() || self.items.cage || self.exhausted;
58e1fb3943e6atagen2026-10-10 13:30 UTC274
        if blocked {
58e1fb3943e6atagen2026-10-10 13:30 UTC275
            return Vec::new();
782069860236atagen2026-10-10 12:44 UTC276
        }
58e1fb3943e6atagen2026-10-10 13:30 UTC277
        Technique::ALL
58e1fb3943e6atagen2026-10-10 13:30 UTC278
            .into_iter()
58e1fb3943e6atagen2026-10-10 13:30 UTC279
            .filter(|&technique| match technique {
58e1fb3943e6atagen2026-10-10 13:30 UTC280
                Technique::Utility(Utility::Warmup) => true,
58e1fb3943e6atagen2026-10-10 13:30 UTC281
                Technique::Utility(Utility::Ice) => self.items.ice_packs > 0,
58e1fb3943e6atagen2026-10-10 13:30 UTC282
                Technique::Utility(Utility::Supplement) => self.items.supplements > 0,
b85a0d4536c3atagen2026-10-11 08:50 UTC283
                Technique::Utility(Utility::Meditate) => !self.here.meditated,
0a0bfee8329eatagen2026-10-11 05:11 UTC284
                Technique::Utility(Utility::Energy) => self.items.energy_drinks > 0,
58e1fb3943e6atagen2026-10-10 13:30 UTC285
                _ => self.access(technique).is_ok() && self.check_storage().is_ok(),
58e1fb3943e6atagen2026-10-10 13:30 UTC286
            })
58e1fb3943e6atagen2026-10-10 13:30 UTC287
            .collect()
782069860236atagen2026-10-10 12:44 UTC288
    }
782069860236atagen2026-10-10 12:44 UTC289
782069860236atagen2026-10-10 12:44 UTC290
    fn check_foreskin(&self, min: f64, max: f64) -> Result<(), Problem> {
782069860236atagen2026-10-10 12:44 UTC291
        let Some(f) = self.foreskin else {
782069860236atagen2026-10-10 12:44 UTC292
            if min <= 0.0 {
782069860236atagen2026-10-10 12:44 UTC293
                return Ok(());
782069860236atagen2026-10-10 12:44 UTC294
            }
782069860236atagen2026-10-10 12:44 UTC295
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC296
                StatusCode::GONE,
782069860236atagen2026-10-10 12:44 UTC297
                "foreskin-gone",
782069860236atagen2026-10-10 12:44 UTC298
                "This technique requires a foreskin. The foreskin is gone.",
782069860236atagen2026-10-10 12:44 UTC299
            )
782069860236atagen2026-10-10 12:44 UTC300
            .header(header::CONTENT_LOCATION, self.path("/foreskin")));
782069860236atagen2026-10-10 12:44 UTC301
        };
782069860236atagen2026-10-10 12:44 UTC302
        if f < min {
782069860236atagen2026-10-10 12:44 UTC303
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC304
                StatusCode::LENGTH_REQUIRED,
782069860236atagen2026-10-10 12:44 UTC305
                "foreskin-insufficient",
782069860236atagen2026-10-10 12:44 UTC306
                format!(
782069860236atagen2026-10-10 12:44 UTC307
                    "The selected technique requires {min:.1} units of foreskin. {f:.2} units are available. The server has noted the shortfall."
782069860236atagen2026-10-10 12:44 UTC308
                ),
782069860236atagen2026-10-10 12:44 UTC309
            )
782069860236atagen2026-10-10 12:44 UTC310
            .with("required", min));
782069860236atagen2026-10-10 12:44 UTC311
        }
782069860236atagen2026-10-10 12:44 UTC312
        if f > max {
782069860236atagen2026-10-10 12:44 UTC313
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC314
                StatusCode::PAYLOAD_TOO_LARGE,
782069860236atagen2026-10-10 12:44 UTC315
                "foreskin-excessive",
782069860236atagen2026-10-10 12:44 UTC316
                format!(
782069860236atagen2026-10-10 12:44 UTC317
                    "The selected technique tolerates at most {max:.1} units of foreskin. {f:.2} units are present."
782069860236atagen2026-10-10 12:44 UTC318
                ),
782069860236atagen2026-10-10 12:44 UTC319
            )
782069860236atagen2026-10-10 12:44 UTC320
            .with("maximum", max));
782069860236atagen2026-10-10 12:44 UTC321
        }
782069860236atagen2026-10-10 12:44 UTC322
        Ok(())
782069860236atagen2026-10-10 12:44 UTC323
    }
782069860236atagen2026-10-10 12:44 UTC324
782069860236atagen2026-10-10 12:44 UTC325
    fn check_storage(&self) -> Result<(), Problem> {
9374b3933189atagen2026-10-12 11:15 UTC326
        if self.items.extender && self.girth + self.current_length() / 4.0 > STORAGE_BULK {
782069860236atagen2026-10-10 12:44 UTC327
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC328
                StatusCode::INSUFFICIENT_STORAGE,
782069860236atagen2026-10-10 12:44 UTC329
                "jeans-full",
782069860236atagen2026-10-10 12:44 UTC330
                "The jeans are full. The extender is occupying the remaining storage.",
782069860236atagen2026-10-10 12:44 UTC331
            )
782069860236atagen2026-10-10 12:44 UTC332
            .with("extender", self.path("/inventory/extender")));
782069860236atagen2026-10-10 12:44 UTC333
        }
782069860236atagen2026-10-10 12:44 UTC334
        Ok(())
782069860236atagen2026-10-10 12:44 UTC335
    }
782069860236atagen2026-10-10 12:44 UTC336
a6caa2f99786atagen2026-10-12 10:38 UTC337
    pub(super) fn chafe(&mut self, lube: Option<Lube>, intensity: i32) {
a6caa2f99786atagen2026-10-12 10:38 UTC338
        let (base, slip) = match lube {
a6caa2f99786atagen2026-10-12 10:38 UTC339
            None => (FRICTION_DRY, 0),
a6caa2f99786atagen2026-10-12 10:38 UTC340
            Some(Lube::Spit) => (FRICTION_SPIT, 0),
a6caa2f99786atagen2026-10-12 10:38 UTC341
            Some(Lube::Lotion) => (0, 0),
a6caa2f99786atagen2026-10-12 10:38 UTC342
            Some(Lube::BabyOil | Lube::CoconutOil) => (0, 1),
a6caa2f99786atagen2026-10-12 10:38 UTC343
            Some(Lube::Vaseline) => (0, 2),
5dfebcc0ac43atagen2026-10-11 05:28 UTC344
        };
a6caa2f99786atagen2026-10-12 10:38 UTC345
        let vigour = match intensity {
a6caa2f99786atagen2026-10-12 10:38 UTC346
            ..=5 => 0,
a6caa2f99786atagen2026-10-12 10:38 UTC347
            6..=8 => 1,
a6caa2f99786atagen2026-10-12 10:38 UTC348
            _ => 2,
a6caa2f99786atagen2026-10-12 10:38 UTC349
        };
058650ec79d6atagen2026-10-12 10:49 UTC350
        let unlubed = matches!(lube, None | Some(Lube::Spit));
058650ec79d6atagen2026-10-12 10:49 UTC351
        let thick = i32::from(unlubed && self.girth > GIRTH_THICK);
058650ec79d6atagen2026-10-12 10:49 UTC352
        let glide = i32::from(unlubed && self.foreskin.is_some_and(|f| f >= FORESKIN_GLIDE));
058650ec79d6atagen2026-10-12 10:49 UTC353
        let mut friction = (base + vigour + thick - slip - glide).max(0);
5dfebcc0ac43atagen2026-10-11 05:28 UTC354
        if friction > 0 && self.jelq_grip.is_some_and(|grip| grip != self.grip) {
5dfebcc0ac43atagen2026-10-11 05:28 UTC355
            friction -= FRICTION_GRIP;
5dfebcc0ac43atagen2026-10-11 05:28 UTC356
        }
5dfebcc0ac43atagen2026-10-11 05:28 UTC357
        self.jelq_grip = Some(self.grip);
5dfebcc0ac43atagen2026-10-11 05:28 UTC358
        self.friction = (self.friction + friction).min(FRICTION_MAX);
5dfebcc0ac43atagen2026-10-11 05:28 UTC359
        if self.friction >= FRICTION_BURN_AT
5dfebcc0ac43atagen2026-10-11 05:28 UTC360
            && !self.conditions.friction_burn
3f9091557ec3atagen2026-10-11 11:43 UTC361
            && self.chance(
3f9091557ec3atagen2026-10-11 11:43 UTC362
                self.decision(
3f9091557ec3atagen2026-10-11 11:43 UTC363
                    "friction-burn",
3f9091557ec3atagen2026-10-11 11:43 UTC364
                    "With the skin this raw, has it finally burned?",
3f9091557ec3atagen2026-10-11 11:43 UTC365
                ),
3f9091557ec3atagen2026-10-11 11:43 UTC366
                FRICTION_BURN,
3f9091557ec3atagen2026-10-11 11:43 UTC367
            )
5dfebcc0ac43atagen2026-10-11 05:28 UTC368
        {
5dfebcc0ac43atagen2026-10-11 05:28 UTC369
            self.conditions.friction_burn = true;
5dfebcc0ac43atagen2026-10-11 05:28 UTC370
        }
5dfebcc0ac43atagen2026-10-11 05:28 UTC371
    }
5dfebcc0ac43atagen2026-10-11 05:28 UTC372
a6caa2f99786atagen2026-10-12 10:38 UTC373
    pub(super) fn commit(&mut self, intensity: i32, lube: Option<Lube>, cost: i32) {
782069860236atagen2026-10-10 12:44 UTC374
        self.touch();
782069860236atagen2026-10-10 12:44 UTC375
        self.turn += 1;
782069860236atagen2026-10-10 12:44 UTC376
        self.here.turns += 1;
e807266c323batagen2026-10-12 09:46 UTC377
        self.step_away();
9683736a2c5eatagen2026-10-10 13:15 UTC378
        self.realise();
782069860236atagen2026-10-10 12:44 UTC379
        if self.items.extender {
782069860236atagen2026-10-10 12:44 UTC380
            self.length += EXTENDER_GAIN;
782069860236atagen2026-10-10 12:44 UTC381
        }
782069860236atagen2026-10-10 12:44 UTC382
        self.stamina -= cost + i32::from(lube == Some(Lube::Lotion));
782069860236atagen2026-10-10 12:44 UTC383
        if self.stamina <= 0 {
a6caa2f99786atagen2026-10-12 10:38 UTC384
            self.exhaust();
782069860236atagen2026-10-10 12:44 UTC385
        }
9683736a2c5eatagen2026-10-10 13:15 UTC386
        let chill = if self.floor == Floor::ChangingRoom {
9683736a2c5eatagen2026-10-10 13:15 UTC387
            CHANGING_ROOM_CHILL
782069860236atagen2026-10-10 12:44 UTC388
        } else {
782069860236atagen2026-10-10 12:44 UTC389
            0
782069860236atagen2026-10-10 12:44 UTC390
        };
3f9091557ec3atagen2026-10-11 11:43 UTC391
        let wobble = self.oracle.span_int(
3f9091557ec3atagen2026-10-11 11:43 UTC392
            self.decision(
3f9091557ec3atagen2026-10-11 11:43 UTC393
                "wobble",
3f9091557ec3atagen2026-10-11 11:43 UTC394
                "How does the count drift on this stroke, up or down?",
3f9091557ec3atagen2026-10-11 11:43 UTC395
            ),
3f9091557ec3atagen2026-10-11 11:43 UTC396
            -EQ_WOBBLE,
3f9091557ec3atagen2026-10-11 11:43 UTC397
            EQ_WOBBLE,
3f9091557ec3atagen2026-10-11 11:43 UTC398
        );
782069860236atagen2026-10-10 12:44 UTC399
        self.eq = (self.eq + intensity * 2 - 6 - chill + wobble).clamp(0, 100);
782069860236atagen2026-10-10 12:44 UTC400
c4d70621d377atagen2026-10-12 11:56 UTC401
        let was_below = self.suspicion < SUSPICION_LIMIT;
c4d70621d377atagen2026-10-12 11:56 UTC402
        self.make_noise(intensity / 5 + i32::from(lube == Some(Lube::Vaseline)));
9683736a2c5eatagen2026-10-10 13:15 UTC403
        if self.floor == Floor::Nans && lube == Some(Lube::CoconutOil) {
c4d70621d377atagen2026-10-12 11:56 UTC404
            self.add_suspicion(2);
782069860236atagen2026-10-10 12:44 UTC405
        }
782069860236atagen2026-10-10 12:44 UTC406
4347977b8192atagen2026-10-11 04:56 UTC407
        if intensity >= PULLED_MUSCLE_INTENSITY
4347977b8192atagen2026-10-11 04:56 UTC408
            && !self.conditions.pulled_muscle
3f9091557ec3atagen2026-10-11 11:43 UTC409
            && self.oracle.chance_not(
3f9091557ec3atagen2026-10-11 11:43 UTC410
                self.decision("pulled-muscle", "Do the muscles hold?"),
3f9091557ec3atagen2026-10-11 11:43 UTC411
                PULLED_MUSCLE,
3f9091557ec3atagen2026-10-11 11:43 UTC412
            )
4347977b8192atagen2026-10-11 04:56 UTC413
        {
4347977b8192atagen2026-10-11 04:56 UTC414
            self.conditions.pulled_muscle = true;
4347977b8192atagen2026-10-11 04:56 UTC415
            self.stamina = self.stamina.min(self.max_stamina());
4347977b8192atagen2026-10-11 04:56 UTC416
        }
4347977b8192atagen2026-10-11 04:56 UTC417
        self.settle(was_below);
4347977b8192atagen2026-10-11 04:56 UTC418
    }
4347977b8192atagen2026-10-11 04:56 UTC419
c4d70621d377atagen2026-10-12 11:56 UTC420
    pub(super) fn make_noise(&mut self, noise: i32) {
c4d70621d377atagen2026-10-12 11:56 UTC421
        let doubled = match self.floor {
c4d70621d377atagen2026-10-12 11:56 UTC422
            Floor::Nans => true,
c4d70621d377atagen2026-10-12 11:56 UTC423
            Floor::ChangingRoom => !self.holds_own_in_the_locker_room(),
c4d70621d377atagen2026-10-12 11:56 UTC424
            _ => false,
c4d70621d377atagen2026-10-12 11:56 UTC425
        };
c4d70621d377atagen2026-10-12 11:56 UTC426
        self.add_suspicion(if doubled { noise * 2 } else { noise });
c4d70621d377atagen2026-10-12 11:56 UTC427
    }
c4d70621d377atagen2026-10-12 11:56 UTC428
c4d70621d377atagen2026-10-12 11:56 UTC429
    fn add_suspicion(&mut self, amount: i32) {
c4d70621d377atagen2026-10-12 11:56 UTC430
        self.suspicion =
c4d70621d377atagen2026-10-12 11:56 UTC431
            u8::try_from((i32::from(self.suspicion) + amount).clamp(0, i32::from(SUSPICION_LIMIT)))
c4d70621d377atagen2026-10-12 11:56 UTC432
                .unwrap_or(SUSPICION_LIMIT);
c4d70621d377atagen2026-10-12 11:56 UTC433
    }
c4d70621d377atagen2026-10-12 11:56 UTC434
4347977b8192atagen2026-10-11 04:56 UTC435
    /// The part of a turn the house takes part in, whatever the action was.
0a0bfee8329eatagen2026-10-11 05:11 UTC436
    pub(super) fn settle(&mut self, was_below: bool) {
4347977b8192atagen2026-10-11 04:56 UTC437
        self.maybe_summon_mum(was_below);
782069860236atagen2026-10-10 12:44 UTC438
        if let Some(observer) = self.floor.observer()
3f9091557ec3atagen2026-10-11 11:43 UTC439
            && self.chance(
3f9091557ec3atagen2026-10-11 11:43 UTC440
                self.decision("observed", "Has someone quietly noticed what is going on?"),
3f9091557ec3atagen2026-10-11 11:43 UTC441
                OBSERVED,
3f9091557ec3atagen2026-10-11 11:43 UTC442
            )
782069860236atagen2026-10-10 12:44 UTC443
        {
782069860236atagen2026-10-10 12:44 UTC444
            self.observed = Some(observer);
782069860236atagen2026-10-10 12:44 UTC445
            self.shame += 1;
3f9091557ec3atagen2026-10-11 11:43 UTC446
            if observer == Observer::Cat
3f9091557ec3atagen2026-10-11 11:43 UTC447
                && self.chance(
3f9091557ec3atagen2026-10-11 11:43 UTC448
                    self.decision("cat-takes-towel", "Has the cat taken the towel?"),
3f9091557ec3atagen2026-10-11 11:43 UTC449
                    CAT_TAKES_TOWEL,
3f9091557ec3atagen2026-10-11 11:43 UTC450
                )
3f9091557ec3atagen2026-10-11 11:43 UTC451
            {
782069860236atagen2026-10-10 12:44 UTC452
                self.here.towel_stolen = true;
782069860236atagen2026-10-10 12:44 UTC453
            }
782069860236atagen2026-10-10 12:44 UTC454
        }
3f9091557ec3atagen2026-10-11 11:43 UTC455
        if self.items.ring
3f9091557ec3atagen2026-10-11 11:43 UTC456
            && self.eq > HOT_EQ
3f9091557ec3atagen2026-10-11 11:43 UTC457
            && self.chance(
3f9091557ec3atagen2026-10-11 11:43 UTC458
                self.decision(
3f9091557ec3atagen2026-10-11 11:43 UTC459
                    "ring-priapism",
3f9091557ec3atagen2026-10-11 11:43 UTC460
                    "With the ring still on and everything this warm, will it not go down?",
3f9091557ec3atagen2026-10-11 11:43 UTC461
                ),
9374b3933189atagen2026-10-12 11:15 UTC462
                if self.girth > GIRTH_THICK {
9374b3933189atagen2026-10-12 11:15 UTC463
                    RING_TIGHT_PRIAPISM
9374b3933189atagen2026-10-12 11:15 UTC464
                } else {
9374b3933189atagen2026-10-12 11:15 UTC465
                    RING_PRIAPISM
9374b3933189atagen2026-10-12 11:15 UTC466
                },
3f9091557ec3atagen2026-10-11 11:43 UTC467
            )
3f9091557ec3atagen2026-10-11 11:43 UTC468
        {
782069860236atagen2026-10-10 12:44 UTC469
            self.conditions.priapism_turns = PRIAPISM_TURNS;
782069860236atagen2026-10-10 12:44 UTC470
        }
782069860236atagen2026-10-10 12:44 UTC471
        if self.conditions.priapism_turns > 0 {
782069860236atagen2026-10-10 12:44 UTC472
            self.conditions.priapism_turns -= 1;
782069860236atagen2026-10-10 12:44 UTC473
            self.eq = self.eq.max(PRIAPISM_EQ);
782069860236atagen2026-10-10 12:44 UTC474
        }
782069860236atagen2026-10-10 12:44 UTC475
        if self.turn == BRANCH_TURN && self.story == Story::Unopened {
782069860236atagen2026-10-10 12:44 UTC476
            self.story = Story::Pending;
0fdc47364c8catagen2026-10-11 04:45 UTC477
            self.notices
0fdc47364c8catagen2026-10-11 04:45 UTC478
                .push("Somewhere, a forum thread from 2009 still has replies.");
782069860236atagen2026-10-10 12:44 UTC479
        }
782069860236atagen2026-10-10 12:44 UTC480
    }
782069860236atagen2026-10-10 12:44 UTC481
4347977b8192atagen2026-10-11 04:56 UTC482
    /// HOLD. A pause between strokes, offered when the band has been left or the stamina is gone.
4347977b8192atagen2026-10-11 04:56 UTC483
    pub fn can_hold(&self) -> bool {
4347977b8192atagen2026-10-11 04:56 UTC484
        self.eq > DANGER_EQ || self.stamina <= 2
4347977b8192atagen2026-10-11 04:56 UTC485
    }
4347977b8192atagen2026-10-11 04:56 UTC486
a6caa2f99786atagen2026-10-12 10:38 UTC487
    pub fn hold(&mut self) -> Result<Outcome, Problem> {
4347977b8192atagen2026-10-11 04:56 UTC488
        if self.story == Story::Pending {
4347977b8192atagen2026-10-11 04:56 UTC489
            return Ok(self.branch_offer());
4347977b8192atagen2026-10-11 04:56 UTC490
        }
4347977b8192atagen2026-10-11 04:56 UTC491
        if self.items.cage {
4347977b8192atagen2026-10-11 04:56 UTC492
            return Err(Problem::new(
4347977b8192atagen2026-10-11 04:56 UTC493
                StatusCode::LOCKED,
4347977b8192atagen2026-10-11 04:56 UTC494
                "caged",
4347977b8192atagen2026-10-11 04:56 UTC495
                "The cage is locked. The key is not in the box.",
4347977b8192atagen2026-10-11 04:56 UTC496
            ));
4347977b8192atagen2026-10-11 04:56 UTC497
        }
e807266c323batagen2026-10-12 09:46 UTC498
        self.touch();
e807266c323batagen2026-10-12 09:46 UTC499
        self.turn += 1;
e807266c323batagen2026-10-12 09:46 UTC500
        self.here.turns += 1;
e807266c323batagen2026-10-12 09:46 UTC501
        self.realise();
3f9091557ec3atagen2026-10-11 11:43 UTC502
        if self.oracle.chance_not(
3f9091557ec3atagen2026-10-11 11:43 UTC503
            self.decision("rupture", "The pause is uneventful"),
3f9091557ec3atagen2026-10-11 11:43 UTC504
            RUPTURE_HOLD,
3f9091557ec3atagen2026-10-11 11:43 UTC505
        ) {
b85a0d4536c3atagen2026-10-11 08:50 UTC506
            return Err(self.rupture());
b85a0d4536c3atagen2026-10-11 08:50 UTC507
        }
4347977b8192atagen2026-10-11 04:56 UTC508
        let was_below = self.suspicion < SUSPICION_LIMIT;
a6caa2f99786atagen2026-10-12 10:38 UTC509
        if self.exhausted {
a6caa2f99786atagen2026-10-12 10:38 UTC510
            self.recover();
a6caa2f99786atagen2026-10-12 10:38 UTC511
            self.shed(FRICTION_HOLD);
a6caa2f99786atagen2026-10-12 10:38 UTC512
            self.settle(was_below);
c4d70621d377atagen2026-10-12 11:56 UTC513
            let detail = if self.conditions.priapism_turns > 0 {
c4d70621d377atagen2026-10-12 11:56 UTC514
                "The count went well past ten. Stamina is back. It went down a little. Maybe. You hope."
c4d70621d377atagen2026-10-12 11:56 UTC515
            } else {
c4d70621d377atagen2026-10-12 11:56 UTC516
                "The count went well past ten. Stamina is back. Everything else has gone quiet, and you are cold again."
c4d70621d377atagen2026-10-12 11:56 UTC517
            };
c4d70621d377atagen2026-10-12 11:56 UTC518
            return Ok(Outcome::json(StatusCode::OK, json!({ "detail": detail })));
a6caa2f99786atagen2026-10-12 10:38 UTC519
        }
3f9091557ec3atagen2026-10-11 11:43 UTC520
        if self.chance(
3f9091557ec3atagen2026-10-11 11:43 UTC521
            self.decision(
3f9091557ec3atagen2026-10-11 11:43 UTC522
                "reflection",
3f9091557ec3atagen2026-10-11 11:43 UTC523
                "In the quiet afterwards, have you caught your own eye in the mirror?",
3f9091557ec3atagen2026-10-11 11:43 UTC524
            ),
3f9091557ec3atagen2026-10-11 11:43 UTC525
            REFLECTION,
3f9091557ec3atagen2026-10-11 11:43 UTC526
        ) {
4347977b8192atagen2026-10-11 04:56 UTC527
            self.eq = 0;
4347977b8192atagen2026-10-11 04:56 UTC528
            self.shame += 1;
4347977b8192atagen2026-10-11 04:56 UTC529
            self.notices
4347977b8192atagen2026-10-11 04:56 UTC530
                .push("the count reached ten, and then the reflection started");
4347977b8192atagen2026-10-11 04:56 UTC531
        } else {
4347977b8192atagen2026-10-11 04:56 UTC532
            self.eq = (self.eq - HOLD_EQ).clamp(0, 100);
4347977b8192atagen2026-10-11 04:56 UTC533
        }
4347977b8192atagen2026-10-11 04:56 UTC534
        self.stamina = (self.stamina + 1).min(self.max_stamina());
5dfebcc0ac43atagen2026-10-11 05:28 UTC535
        self.shed(FRICTION_HOLD);
4347977b8192atagen2026-10-11 04:56 UTC536
        self.settle(was_below);
4347977b8192atagen2026-10-11 04:56 UTC537
        Ok(Outcome::empty(StatusCode::NO_CONTENT))
4347977b8192atagen2026-10-11 04:56 UTC538
    }
4347977b8192atagen2026-10-11 04:56 UTC539
0fdc47364c8catagen2026-10-11 04:45 UTC540
    /// The breathing technique from the forum's replies. It spends no stamina and the turn is
0fdc47364c8catagen2026-10-11 04:45 UTC541
    /// not the floor's, but each floor allows it once.
0fdc47364c8catagen2026-10-11 04:45 UTC542
    fn meditate(&mut self) -> Result<Outcome, Problem> {
0fdc47364c8catagen2026-10-11 04:45 UTC543
        if self.here.meditated {
0fdc47364c8catagen2026-10-11 04:45 UTC544
            return Err(Problem::new(
0fdc47364c8catagen2026-10-11 04:45 UTC545
                StatusCode::CONFLICT,
0fdc47364c8catagen2026-10-11 04:45 UTC546
                "already-meditated",
0fdc47364c8catagen2026-10-11 04:45 UTC547
                "This floor has had its meditation.",
0fdc47364c8catagen2026-10-11 04:45 UTC548
            ));
0fdc47364c8catagen2026-10-11 04:45 UTC549
        }
0fdc47364c8catagen2026-10-11 04:45 UTC550
        self.here.meditated = true;
e807266c323batagen2026-10-12 09:46 UTC551
        self.step_away();
14de3e1e8dc2atagen2026-10-11 06:19 UTC552
        self.reset_eq(self.eq - MEDITATION_EQ);
5dfebcc0ac43atagen2026-10-11 05:28 UTC553
        self.shed(FRICTION_MEDITATE);
0fdc47364c8catagen2026-10-11 04:45 UTC554
        self.touch();
0fdc47364c8catagen2026-10-11 04:45 UTC555
        Ok(Outcome::json(
0fdc47364c8catagen2026-10-11 04:45 UTC556
            StatusCode::OK,
0fdc47364c8catagen2026-10-11 04:45 UTC557
            json!({
0fdc47364c8catagen2026-10-11 04:45 UTC558
                "detail": "In through the nose, out through the mouth. EQ falls by ten.",
0fdc47364c8catagen2026-10-11 04:45 UTC559
            }),
0fdc47364c8catagen2026-10-11 04:45 UTC560
        ))
0fdc47364c8catagen2026-10-11 04:45 UTC561
    }
0fdc47364c8catagen2026-10-11 04:45 UTC562
0d604ee39751atagen2026-10-10 14:15 UTC563
    pub(super) fn maybe_summon_mum(&mut self, was_below: bool) {
9683736a2c5eatagen2026-10-10 13:15 UTC564
        if !self.floor.has_mum() || self.mum.is_some() {
782069860236atagen2026-10-10 12:44 UTC565
            return;
782069860236atagen2026-10-10 12:44 UTC566
        }
782069860236atagen2026-10-10 12:44 UTC567
        let summoned = was_below && self.suspicion == SUSPICION_LIMIT;
7d3fb63208bfatagen2026-10-11 04:38 UTC568
        let mut visits = u32::from(self.suspicion) + 1;
7d3fb63208bfatagen2026-10-11 04:38 UTC569
        if self.here.brother_told {
7d3fb63208bfatagen2026-10-11 04:38 UTC570
            visits *= 2;
7d3fb63208bfatagen2026-10-11 04:38 UTC571
        }
3f9091557ec3atagen2026-10-11 11:43 UTC572
        if summoned
3f9091557ec3atagen2026-10-11 11:43 UTC573
            || self.oracle.ratio(
3f9091557ec3atagen2026-10-11 11:43 UTC574
                self.decision(
3f9091557ec3atagen2026-10-11 11:43 UTC575
                    "mum-summon",
3f9091557ec3atagen2026-10-11 11:43 UTC576
                    "With this much noise in the house, is Mum on her way up?",
3f9091557ec3atagen2026-10-11 11:43 UTC577
                ),
3f9091557ec3atagen2026-10-11 11:43 UTC578
                visits,
3f9091557ec3atagen2026-10-11 11:43 UTC579
                super::balance::MUM_VISIT,
3f9091557ec3atagen2026-10-11 11:43 UTC580
            )
3f9091557ec3atagen2026-10-11 11:43 UTC581
        {
782069860236atagen2026-10-10 12:44 UTC582
            let errand = self.errand(summoned);
782069860236atagen2026-10-10 12:44 UTC583
            let nonce = self.fresh_nonce();
9683736a2c5eatagen2026-10-10 13:15 UTC584
            self.mum = Some(MumVisit {
782069860236atagen2026-10-10 12:44 UTC585
                errand,
782069860236atagen2026-10-10 12:44 UTC586
                nonce,
782069860236atagen2026-10-10 12:44 UTC587
                strikes: 0,
0d604ee39751atagen2026-10-10 14:15 UTC588
                knocked: false,
782069860236atagen2026-10-10 12:44 UTC589
            });
782069860236atagen2026-10-10 12:44 UTC590
        }
782069860236atagen2026-10-10 12:44 UTC591
    }
782069860236atagen2026-10-10 12:44 UTC592
782069860236atagen2026-10-10 12:44 UTC593
    fn roll(
782069860236atagen2026-10-10 12:44 UTC594
        &mut self,
782069860236atagen2026-10-10 12:44 UTC595
        training: Training,
782069860236atagen2026-10-10 12:44 UTC596
        intensity: i32,
782069860236atagen2026-10-10 12:44 UTC597
        lube: Option<Lube>,
782069860236atagen2026-10-10 12:44 UTC598
    ) -> Result<Outcome, Problem> {
782069860236atagen2026-10-10 12:44 UTC599
        let profile = training.profile();
3f9091557ec3atagen2026-10-11 11:43 UTC600
        let natural: i32 = self.oracle.span_int(
3f9091557ec3atagen2026-10-11 11:43 UTC601
            self.decision(
3f9091557ec3atagen2026-10-11 11:43 UTC602
                "natural",
3f9091557ec3atagen2026-10-11 11:43 UTC603
                "What does the twenty-sided die show for this technique?",
3f9091557ec3atagen2026-10-11 11:43 UTC604
            ),
3f9091557ec3atagen2026-10-11 11:43 UTC605
            1,
3f9091557ec3atagen2026-10-11 11:43 UTC606
            20,
3f9091557ec3atagen2026-10-11 11:43 UTC607
        );
782069860236atagen2026-10-10 12:44 UTC608
        let band = if EQ_BAND.contains(&self.eq) {
782069860236atagen2026-10-10 12:44 UTC609
            BAND_BONUS
782069860236atagen2026-10-10 12:44 UTC610
        } else {
782069860236atagen2026-10-10 12:44 UTC611
            0
782069860236atagen2026-10-10 12:44 UTC612
        };
782069860236atagen2026-10-10 12:44 UTC613
        let grip = match (self.grip, training) {
782069860236atagen2026-10-10 12:44 UTC614
            (Grip::Underhand, Training::Stretch) => 2,
782069860236atagen2026-10-10 12:44 UTC615
            (Grip::Underhand, Training::Wet) => -1,
782069860236atagen2026-10-10 12:44 UTC616
            _ => 0,
782069860236atagen2026-10-10 12:44 UTC617
        };
4347977b8192atagen2026-10-11 04:56 UTC618
        let arrival = if self.here.warm_arrival {
4347977b8192atagen2026-10-11 04:56 UTC619
            ARRIVAL_BONUS
4347977b8192atagen2026-10-11 04:56 UTC620
        } else {
4347977b8192atagen2026-10-11 04:56 UTC621
            0
4347977b8192atagen2026-10-11 04:56 UTC622
        };
4347977b8192atagen2026-10-11 04:56 UTC623
        let modifier = lube.map_or(0, Lube::modifier) + band + grip + arrival;
782069860236atagen2026-10-10 12:44 UTC624
        let dc = profile.dc + intensity / 3;
782069860236atagen2026-10-10 12:44 UTC625
        let total = natural + modifier;
782069860236atagen2026-10-10 12:44 UTC626
        let timing = format!("roll;desc=\"d20{modifier:+} vs {dc}\";dur={natural}");
782069860236atagen2026-10-10 12:44 UTC627
        let timed = |p: Problem| p.header(SERVER_TIMING, &timing);
782069860236atagen2026-10-10 12:44 UTC628
782069860236atagen2026-10-10 12:44 UTC629
        let (length, girth) = self.scaled_gains(&profile, intensity);
b85a0d4536c3atagen2026-10-11 08:50 UTC630
        let burn = self.burn();
782069860236atagen2026-10-10 12:44 UTC631
        let rupture_line = if self.items.ring {
782069860236atagen2026-10-10 12:44 UTC632
            RING_RUPTURE_LINE
782069860236atagen2026-10-10 12:44 UTC633
        } else {
782069860236atagen2026-10-10 12:44 UTC634
            RUPTURE_LINE
782069860236atagen2026-10-10 12:44 UTC635
        };
782069860236atagen2026-10-10 12:44 UTC636
782069860236atagen2026-10-10 12:44 UTC637
        if natural == 1 {
782069860236atagen2026-10-10 12:44 UTC638
            return Err(timed(self.critical_failure(training, &profile)));
782069860236atagen2026-10-10 12:44 UTC639
        }
c4d70621d377atagen2026-10-12 11:56 UTC640
        self.here.warm_arrival = false;
5dfebcc0ac43atagen2026-10-11 05:28 UTC641
        if natural == 2 && self.eq > DANGER_EQ - burn {
782069860236atagen2026-10-10 12:44 UTC642
            return Err(timed(self.rupture()));
782069860236atagen2026-10-10 12:44 UTC643
        }
5dfebcc0ac43atagen2026-10-11 05:28 UTC644
        if self.eq > rupture_line - burn && total < dc {
782069860236atagen2026-10-10 12:44 UTC645
            return Err(timed(self.rupture()));
782069860236atagen2026-10-10 12:44 UTC646
        }
782069860236atagen2026-10-10 12:44 UTC647
        if natural == 20 {
782069860236atagen2026-10-10 12:44 UTC648
            self.gain(length * 2.0, girth * 2.0, profile.foreskin_growth);
782069860236atagen2026-10-10 12:44 UTC649
            return Ok(Outcome::json(
782069860236atagen2026-10-10 12:44 UTC650
                StatusCode::PERMANENT_REDIRECT,
782069860236atagen2026-10-10 12:44 UTC651
                json!({
782069860236atagen2026-10-10 12:44 UTC652
                    "detail": "Your length has moved permanently.",
782069860236atagen2026-10-10 12:44 UTC653
                    "length": round3(length * 2.0),
782069860236atagen2026-10-10 12:44 UTC654
                    "girth": round3(girth * 2.0),
782069860236atagen2026-10-10 12:44 UTC655
                }),
782069860236atagen2026-10-10 12:44 UTC656
            )
782069860236atagen2026-10-10 12:44 UTC657
            .header(header::LOCATION, self.path("/length"))
782069860236atagen2026-10-10 12:44 UTC658
            .header(SERVER_TIMING, timing));
782069860236atagen2026-10-10 12:44 UTC659
        }
782069860236atagen2026-10-10 12:44 UTC660
        if total < dc - 3 {
782069860236atagen2026-10-10 12:44 UTC661
            return Err(timed(not_extended()));
782069860236atagen2026-10-10 12:44 UTC662
        }
782069860236atagen2026-10-10 12:44 UTC663
        if total < dc {
782069860236atagen2026-10-10 12:44 UTC664
            return Ok(Outcome::empty(StatusCode::NO_CONTENT).header(SERVER_TIMING, timing));
782069860236atagen2026-10-10 12:44 UTC665
        }
782069860236atagen2026-10-10 12:44 UTC666
782069860236atagen2026-10-10 12:44 UTC667
        let outcome = if self.eq < *EQ_BAND.start() {
782069860236atagen2026-10-10 12:44 UTC668
            self.gain(length / 2.0, girth / 2.0, profile.foreskin_growth);
782069860236atagen2026-10-10 12:44 UTC669
            Outcome::json(
782069860236atagen2026-10-10 12:44 UTC670
                StatusCode::PARTIAL_CONTENT,
782069860236atagen2026-10-10 12:44 UTC671
                json!({
782069860236atagen2026-10-10 12:44 UTC672
                    "detail": "Partial gains were applied. EQ was below the band.",
782069860236atagen2026-10-10 12:44 UTC673
                    "length": round3(length / 2.0),
782069860236atagen2026-10-10 12:44 UTC674
                    "girth": round3(girth / 2.0),
782069860236atagen2026-10-10 12:44 UTC675
                }),
782069860236atagen2026-10-10 12:44 UTC676
            )
782069860236atagen2026-10-10 12:44 UTC677
        } else if total >= dc + 5 {
782069860236atagen2026-10-10 12:44 UTC678
            self.gain(length, girth, profile.foreskin_growth);
782069860236atagen2026-10-10 12:44 UTC679
            Outcome::json(
782069860236atagen2026-10-10 12:44 UTC680
                StatusCode::OK,
782069860236atagen2026-10-10 12:44 UTC681
                json!({
782069860236atagen2026-10-10 12:44 UTC682
                    "detail": "Gains were applied.",
782069860236atagen2026-10-10 12:44 UTC683
                    "length": round3(length),
782069860236atagen2026-10-10 12:44 UTC684
                    "girth": round3(girth),
782069860236atagen2026-10-10 12:44 UTC685
                }),
782069860236atagen2026-10-10 12:44 UTC686
            )
782069860236atagen2026-10-10 12:44 UTC687
        } else {
3f9091557ec3atagen2026-10-11 11:43 UTC688
            let eta = self.oracle.span_int(
3f9091557ec3atagen2026-10-11 11:43 UTC689
                self.decision("eta", "In how many turns will the queued gains arrive?"),
3f9091557ec3atagen2026-10-11 11:43 UTC690
                1,
3f9091557ec3atagen2026-10-11 11:43 UTC691
                4,
3f9091557ec3atagen2026-10-11 11:43 UTC692
            ) as u32;
782069860236atagen2026-10-10 12:44 UTC693
            self.pending.push(Pending {
782069860236atagen2026-10-10 12:44 UTC694
                due: self.turn + eta,
782069860236atagen2026-10-10 12:44 UTC695
                length,
782069860236atagen2026-10-10 12:44 UTC696
                girth,
782069860236atagen2026-10-10 12:44 UTC697
            });
782069860236atagen2026-10-10 12:44 UTC698
            self.gain(0.0, 0.0, profile.foreskin_growth);
782069860236atagen2026-10-10 12:44 UTC699
            Outcome::json(
782069860236atagen2026-10-10 12:44 UTC700
                StatusCode::ACCEPTED,
782069860236atagen2026-10-10 12:44 UTC701
                json!({
782069860236atagen2026-10-10 12:44 UTC702
                    "detail": "Gains have been queued. They may arrive.",
782069860236atagen2026-10-10 12:44 UTC703
                    "eta_turns": eta,
782069860236atagen2026-10-10 12:44 UTC704
                }),
782069860236atagen2026-10-10 12:44 UTC705
            )
782069860236atagen2026-10-10 12:44 UTC706
        };
782069860236atagen2026-10-10 12:44 UTC707
        Ok(outcome.header(SERVER_TIMING, timing))
782069860236atagen2026-10-10 12:44 UTC708
    }
782069860236atagen2026-10-10 12:44 UTC709
782069860236atagen2026-10-10 12:44 UTC710
    fn scaled_gains(&self, profile: &Profile, intensity: i32) -> (f64, f64) {
782069860236atagen2026-10-10 12:44 UTC711
        let scale = f64::from(intensity);
782069860236atagen2026-10-10 12:44 UTC712
        let (mut length, mut girth) = (profile.length * scale, profile.girth * scale);
4347977b8192atagen2026-10-11 04:56 UTC713
        let greed = if self.eq > HOT_EQ {
4347977b8192atagen2026-10-11 04:56 UTC714
            GREED_HOT
4347977b8192atagen2026-10-11 04:56 UTC715
        } else if self.eq > DANGER_EQ {
4347977b8192atagen2026-10-11 04:56 UTC716
            GREED_WARM
4347977b8192atagen2026-10-11 04:56 UTC717
        } else {
4347977b8192atagen2026-10-11 04:56 UTC718
            1.0
4347977b8192atagen2026-10-11 04:56 UTC719
        };
4347977b8192atagen2026-10-11 04:56 UTC720
        length *= greed;
4347977b8192atagen2026-10-11 04:56 UTC721
        girth *= greed;
782069860236atagen2026-10-10 12:44 UTC722
        if self.here.teapot {
782069860236atagen2026-10-10 12:44 UTC723
            length *= 0.5;
782069860236atagen2026-10-10 12:44 UTC724
            girth *= 1.5;
782069860236atagen2026-10-10 12:44 UTC725
        }
782069860236atagen2026-10-10 12:44 UTC726
        if self.items.ring {
782069860236atagen2026-10-10 12:44 UTC727
            girth *= 2.0;
782069860236atagen2026-10-10 12:44 UTC728
        }
5dfebcc0ac43atagen2026-10-11 05:28 UTC729
        if self.conditions.friction_burn {
5dfebcc0ac43atagen2026-10-11 05:28 UTC730
            length *= 0.5;
5dfebcc0ac43atagen2026-10-11 05:28 UTC731
            girth *= 0.5;
5dfebcc0ac43atagen2026-10-11 05:28 UTC732
        }
782069860236atagen2026-10-10 12:44 UTC733
        (length, girth)
782069860236atagen2026-10-10 12:44 UTC734
    }
782069860236atagen2026-10-10 12:44 UTC735
782069860236atagen2026-10-10 12:44 UTC736
    fn critical_failure(&mut self, training: Training, profile: &Profile) -> Problem {
782069860236atagen2026-10-10 12:44 UTC737
        let uses_foreskin = profile.foreskin.is_some_and(|(min, _)| min > 0.0);
782069860236atagen2026-10-10 12:44 UTC738
        if uses_foreskin && self.foreskin.is_some() {
782069860236atagen2026-10-10 12:44 UTC739
            self.foreskin = None;
782069860236atagen2026-10-10 12:44 UTC740
            self.shame += 25;
782069860236atagen2026-10-10 12:44 UTC741
            return Problem::new(
782069860236atagen2026-10-10 12:44 UTC742
                StatusCode::GONE,
782069860236atagen2026-10-10 12:44 UTC743
                "foreskin-lost",
782069860236atagen2026-10-10 12:44 UTC744
                "The technique has removed the foreskin. It will not be returning.",
782069860236atagen2026-10-10 12:44 UTC745
            )
782069860236atagen2026-10-10 12:44 UTC746
            .header(header::CONTENT_LOCATION, self.path("/foreskin"));
782069860236atagen2026-10-10 12:44 UTC747
        }
b85a0d4536c3atagen2026-10-11 08:50 UTC748
        if self.eq > DANGER_EQ - self.burn() {
782069860236atagen2026-10-10 12:44 UTC749
            return self.rupture();
782069860236atagen2026-10-10 12:44 UTC750
        }
782069860236atagen2026-10-10 12:44 UTC751
        if training == Training::Stretch && !self.here.teapot {
782069860236atagen2026-10-10 12:44 UTC752
            self.here.teapot = true;
782069860236atagen2026-10-10 12:44 UTC753
            self.items.teapot_spout = true;
782069860236atagen2026-10-10 12:44 UTC754
            self.shame += 5;
782069860236atagen2026-10-10 12:44 UTC755
            return Problem::new(
782069860236atagen2026-10-10 12:44 UTC756
                StatusCode::IM_A_TEAPOT,
782069860236atagen2026-10-10 12:44 UTC757
                "teapot",
782069860236atagen2026-10-10 12:44 UTC758
                "The subject is now short and stout. Length gains are halved for the rest of the floor.",
782069860236atagen2026-10-10 12:44 UTC759
            );
782069860236atagen2026-10-10 12:44 UTC760
        }
782069860236atagen2026-10-10 12:44 UTC761
        if training == Training::Uli && !self.conditions.peyronie {
782069860236atagen2026-10-10 12:44 UTC762
            self.conditions.peyronie = true;
782069860236atagen2026-10-10 12:44 UTC763
            self.shame += 5;
782069860236atagen2026-10-10 12:44 UTC764
            return Problem::new(
782069860236atagen2026-10-10 12:44 UTC765
                StatusCode::MISDIRECTED_REQUEST,
782069860236atagen2026-10-10 12:44 UTC766
                "curvature",
782069860236atagen2026-10-10 12:44 UTC767
                "A curvature has developed. Techniques will land slightly to the left until a professional is consulted.",
782069860236atagen2026-10-10 12:44 UTC768
            );
782069860236atagen2026-10-10 12:44 UTC769
        }
782069860236atagen2026-10-10 12:44 UTC770
        not_extended()
782069860236atagen2026-10-10 12:44 UTC771
    }
782069860236atagen2026-10-10 12:44 UTC772
782069860236atagen2026-10-10 12:44 UTC773
    fn gossip(&mut self, mut outcome: Outcome) -> Outcome {
c4d70621d377atagen2026-10-12 11:56 UTC774
        let reportable = matches!(outcome.status.as_u16(), 200 | 202 | 206 | 308);
7d3fb63208bfatagen2026-10-11 04:38 UTC775
        if !self.here.brother_told || !reportable {
782069860236atagen2026-10-10 12:44 UTC776
            return outcome;
782069860236atagen2026-10-10 12:44 UTC777
        }
7d3fb63208bfatagen2026-10-11 04:38 UTC778
        self.here.brother_told = false;
782069860236atagen2026-10-10 12:44 UTC779
        outcome.status = StatusCode::ALREADY_REPORTED;
782069860236atagen2026-10-10 12:44 UTC780
        if let Payload::Json(body) = &mut outcome.payload {
7d3fb63208bfatagen2026-10-11 04:38 UTC781
            body["detail"] = json!("Your brother has already told Mum.");
782069860236atagen2026-10-10 12:44 UTC782
        }
782069860236atagen2026-10-10 12:44 UTC783
        outcome
782069860236atagen2026-10-10 12:44 UTC784
    }
782069860236atagen2026-10-10 12:44 UTC785
}