atagen

jelq

public

an http based roguelike rpg about jelqing

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

Blame: src/game/run.rs

8b8e83329a1datagen2026-10-12 12:01 UTC1
use std::{
8b8e83329a1datagen2026-10-12 12:01 UTC2
    hash::{DefaultHasher, Hash, Hasher},
8b8e83329a1datagen2026-10-12 12:01 UTC3
    time::{Instant, SystemTime},
8b8e83329a1datagen2026-10-12 12:01 UTC4
};
8b8e83329a1datagen2026-10-12 12:01 UTC5
8b8e83329a1datagen2026-10-12 12:01 UTC6
use axum::http::StatusCode;
8b8e83329a1datagen2026-10-12 12:01 UTC7
use serde_json::{Value, json};
8b8e83329a1datagen2026-10-12 12:01 UTC8
8b8e83329a1datagen2026-10-12 12:01 UTC9
use super::{
880b29a0c11eatagen2026-10-13 09:27 UTC10
    Body, Cause, Clinic, Ending, Floor, FloorState, Household, Item, Items, Notice, Observer,
880b29a0c11eatagen2026-10-13 09:27 UTC11
    Online, RunStatus, Standing, Story,
8b8e83329a1datagen2026-10-12 12:01 UTC12
    balance::{
f48daa8adf59atagen2026-10-13 12:51 UTC13
        BRANCH_TURN, FLOOR_LIMIT, MAX_STAMINA, PRIAPISM_EQ, PULLED_STAMINA, REFRACTORY_STAMINA,
f48daa8adf59atagen2026-10-13 12:51 UTC14
        SECOND_WIND, SELF_REPORT_INFLATION, STARTING_BABY_OIL, STARTING_EQ, STARTING_LUBE,
8b8e83329a1datagen2026-10-12 12:01 UTC15
    },
8b8e83329a1datagen2026-10-12 12:01 UTC16
    measure::round2,
8b8e83329a1datagen2026-10-12 12:01 UTC17
    oracle::{Oracle, OracleTemplate, Roll},
8b8e83329a1datagen2026-10-12 12:01 UTC18
};
8b8e83329a1datagen2026-10-12 12:01 UTC19
use crate::problem::Problem;
8b8e83329a1datagen2026-10-12 12:01 UTC20
8b8e83329a1datagen2026-10-12 12:01 UTC21
pub struct Identity {
8b8e83329a1datagen2026-10-12 12:01 UTC22
    /// The private id in the run's URL. Knowing it is the only authorisation a run has.
8b8e83329a1datagen2026-10-12 12:01 UTC23
    pub(crate) id: String,
8b8e83329a1datagen2026-10-12 12:01 UTC24
    pub(crate) public_id: String,
8b8e83329a1datagen2026-10-12 12:01 UTC25
    pub(crate) seeded: bool,
8b8e83329a1datagen2026-10-12 12:01 UTC26
    etag_salt: u64,
8b8e83329a1datagen2026-10-12 12:01 UTC27
    pub(crate) version: u64,
8b8e83329a1datagen2026-10-12 12:01 UTC28
    pub(crate) last_seen: Instant,
8b8e83329a1datagen2026-10-12 12:01 UTC29
}
8b8e83329a1datagen2026-10-12 12:01 UTC30
8b8e83329a1datagen2026-10-12 12:01 UTC31
pub struct Run {
8b8e83329a1datagen2026-10-12 12:01 UTC32
    pub(crate) ident: Identity,
8b8e83329a1datagen2026-10-12 12:01 UTC33
    pub(crate) body: Body,
8b8e83329a1datagen2026-10-12 12:01 UTC34
    pub(crate) standing: Standing,
8b8e83329a1datagen2026-10-12 12:01 UTC35
    pub(crate) household: Household,
8b8e83329a1datagen2026-10-12 12:01 UTC36
    pub(crate) online: Online,
8b8e83329a1datagen2026-10-12 12:01 UTC37
    pub(crate) items: Items,
8b8e83329a1datagen2026-10-12 12:01 UTC38
    pub(crate) clinic: Clinic,
8b8e83329a1datagen2026-10-12 12:01 UTC39
    pub(crate) turn: u32,
8b8e83329a1datagen2026-10-12 12:01 UTC40
    pub(crate) floor: Floor,
8b8e83329a1datagen2026-10-12 12:01 UTC41
    pub(crate) here: FloorState,
8b8e83329a1datagen2026-10-12 12:01 UTC42
    pub(crate) observed: Option<Observer>,
8b8e83329a1datagen2026-10-12 12:01 UTC43
    pub(crate) notices: Vec<Notice>,
8b8e83329a1datagen2026-10-12 12:01 UTC44
    pub(crate) ended: Option<Ending>,
8b8e83329a1datagen2026-10-12 12:01 UTC45
    pub(super) oracle: Oracle,
8b8e83329a1datagen2026-10-12 12:01 UTC46
}
8b8e83329a1datagen2026-10-12 12:01 UTC47
8b8e83329a1datagen2026-10-12 12:01 UTC48
impl Run {
8b8e83329a1datagen2026-10-12 12:01 UTC49
    #[cfg(test)]
8b8e83329a1datagen2026-10-12 12:01 UTC50
    pub fn new(id: String, public_id: String, seed: u64) -> Self {
8b8e83329a1datagen2026-10-12 12:01 UTC51
        Self::with_oracle(id, public_id, seed, false, &OracleTemplate::Rng)
8b8e83329a1datagen2026-10-12 12:01 UTC52
    }
8b8e83329a1datagen2026-10-12 12:01 UTC53
8b8e83329a1datagen2026-10-12 12:01 UTC54
    pub fn with_oracle(
8b8e83329a1datagen2026-10-12 12:01 UTC55
        id: String,
8b8e83329a1datagen2026-10-12 12:01 UTC56
        public_id: String,
8b8e83329a1datagen2026-10-12 12:01 UTC57
        seed: u64,
8b8e83329a1datagen2026-10-12 12:01 UTC58
        seeded: bool,
8b8e83329a1datagen2026-10-12 12:01 UTC59
        template: &OracleTemplate,
8b8e83329a1datagen2026-10-12 12:01 UTC60
    ) -> Self {
8b8e83329a1datagen2026-10-12 12:01 UTC61
        let mut oracle = Oracle::build(template, seed);
8b8e83329a1datagen2026-10-12 12:01 UTC62
        let length = round2(oracle.span(
8b8e83329a1datagen2026-10-12 12:01 UTC63
            Run::unborn("length", "How long is it, in inches?"),
8b8e83329a1datagen2026-10-12 12:01 UTC64
            4.5,
8b8e83329a1datagen2026-10-12 12:01 UTC65
            6.5,
8b8e83329a1datagen2026-10-12 12:01 UTC66
        ));
8b8e83329a1datagen2026-10-12 12:01 UTC67
        let girth = round2(oracle.span(
8b8e83329a1datagen2026-10-12 12:01 UTC68
            Run::unborn("girth", "How thick is it, in inches?"),
8b8e83329a1datagen2026-10-12 12:01 UTC69
            4.0,
8b8e83329a1datagen2026-10-12 12:01 UTC70
            5.0,
8b8e83329a1datagen2026-10-12 12:01 UTC71
        ));
8b8e83329a1datagen2026-10-12 12:01 UTC72
        let foreskin = round2(oracle.span(
8b8e83329a1datagen2026-10-12 12:01 UTC73
            Run::unborn("foreskin", "How much foreskin is there, in inches?"),
8b8e83329a1datagen2026-10-12 12:01 UTC74
            0.4,
8b8e83329a1datagen2026-10-12 12:01 UTC75
            2.4,
8b8e83329a1datagen2026-10-12 12:01 UTC76
        ));
9adb509e6685atagen2026-10-14 08:54 UTC77
        let inflation = f64::from(oracle.span_int(
9adb509e6685atagen2026-10-14 08:54 UTC78
            Run::unborn(
9adb509e6685atagen2026-10-14 08:54 UTC79
                "inflation",
9adb509e6685atagen2026-10-14 08:54 UTC80
                "How far does the self-report round up, in tenths of an inch?",
9adb509e6685atagen2026-10-14 08:54 UTC81
            ),
9adb509e6685atagen2026-10-14 08:54 UTC82
            *SELF_REPORT_INFLATION.start(),
9adb509e6685atagen2026-10-14 08:54 UTC83
            *SELF_REPORT_INFLATION.end(),
9adb509e6685atagen2026-10-14 08:54 UTC84
        )) / 10.0;
880b29a0c11eatagen2026-10-13 09:27 UTC85
        let mut items = Items::default();
880b29a0c11eatagen2026-10-13 09:27 UTC86
        if oracle.chance(
880b29a0c11eatagen2026-10-13 09:27 UTC87
            Run::unborn(
880b29a0c11eatagen2026-10-13 09:27 UTC88
                "starting-lube",
880b29a0c11eatagen2026-10-13 09:27 UTC89
                "Is there a bottle of lube in the drawer already?",
880b29a0c11eatagen2026-10-13 09:27 UTC90
            ),
880b29a0c11eatagen2026-10-13 09:27 UTC91
            STARTING_LUBE,
880b29a0c11eatagen2026-10-13 09:27 UTC92
        ) {
880b29a0c11eatagen2026-10-13 09:27 UTC93
            let baby_oil = oracle.chance(
880b29a0c11eatagen2026-10-13 09:27 UTC94
                Run::unborn(
880b29a0c11eatagen2026-10-13 09:27 UTC95
                    "starting-bottle",
880b29a0c11eatagen2026-10-13 09:27 UTC96
                    "Is the bottle baby oil, rather than lotion?",
880b29a0c11eatagen2026-10-13 09:27 UTC97
                ),
880b29a0c11eatagen2026-10-13 09:27 UTC98
                STARTING_BABY_OIL,
880b29a0c11eatagen2026-10-13 09:27 UTC99
            );
880b29a0c11eatagen2026-10-13 09:27 UTC100
            items.grant(if baby_oil {
880b29a0c11eatagen2026-10-13 09:27 UTC101
                Item::BabyOil
880b29a0c11eatagen2026-10-13 09:27 UTC102
            } else {
880b29a0c11eatagen2026-10-13 09:27 UTC103
                Item::Lotion
880b29a0c11eatagen2026-10-13 09:27 UTC104
            });
880b29a0c11eatagen2026-10-13 09:27 UTC105
        }
8b8e83329a1datagen2026-10-12 12:01 UTC106
        let etag_salt = oracle.u64();
8b8e83329a1datagen2026-10-12 12:01 UTC107
        Self {
8b8e83329a1datagen2026-10-12 12:01 UTC108
            ident: Identity {
8b8e83329a1datagen2026-10-12 12:01 UTC109
                id,
8b8e83329a1datagen2026-10-12 12:01 UTC110
                public_id,
8b8e83329a1datagen2026-10-12 12:01 UTC111
                seeded,
8b8e83329a1datagen2026-10-12 12:01 UTC112
                etag_salt,
8b8e83329a1datagen2026-10-12 12:01 UTC113
                version: 0,
8b8e83329a1datagen2026-10-12 12:01 UTC114
                last_seen: Instant::now(),
8b8e83329a1datagen2026-10-12 12:01 UTC115
            },
9adb509e6685atagen2026-10-14 08:54 UTC116
            body: Body::new(length, girth, foreskin, inflation),
8b8e83329a1datagen2026-10-12 12:01 UTC117
            standing: Standing::default(),
8b8e83329a1datagen2026-10-12 12:01 UTC118
            household: Household::default(),
8b8e83329a1datagen2026-10-12 12:01 UTC119
            online: Online::default(),
880b29a0c11eatagen2026-10-13 09:27 UTC120
            items,
8b8e83329a1datagen2026-10-12 12:01 UTC121
            clinic: Clinic::default(),
8b8e83329a1datagen2026-10-12 12:01 UTC122
            turn: 0,
8b8e83329a1datagen2026-10-12 12:01 UTC123
            floor: Floor::Bedroom,
8b8e83329a1datagen2026-10-12 12:01 UTC124
            here: FloorState::default(),
8b8e83329a1datagen2026-10-12 12:01 UTC125
            observed: None,
8b8e83329a1datagen2026-10-12 12:01 UTC126
            notices: Vec::new(),
8b8e83329a1datagen2026-10-12 12:01 UTC127
            ended: None,
8b8e83329a1datagen2026-10-12 12:01 UTC128
            oracle,
8b8e83329a1datagen2026-10-12 12:01 UTC129
        }
8b8e83329a1datagen2026-10-12 12:01 UTC130
    }
8b8e83329a1datagen2026-10-12 12:01 UTC131
8b8e83329a1datagen2026-10-12 12:01 UTC132
    fn unborn(site: &'static str, ask: &'static str) -> Roll {
8b8e83329a1datagen2026-10-12 12:01 UTC133
        Roll {
8b8e83329a1datagen2026-10-12 12:01 UTC134
            state: "A new run is being created. No game has been played yet.".into(),
8b8e83329a1datagen2026-10-12 12:01 UTC135
            site,
8b8e83329a1datagen2026-10-12 12:01 UTC136
            ask,
8b8e83329a1datagen2026-10-12 12:01 UTC137
        }
8b8e83329a1datagen2026-10-12 12:01 UTC138
    }
8b8e83329a1datagen2026-10-12 12:01 UTC139
8b8e83329a1datagen2026-10-12 12:01 UTC140
    fn etag_at(&self, version: u64) -> String {
8b8e83329a1datagen2026-10-12 12:01 UTC141
        let mut hasher = DefaultHasher::new();
8b8e83329a1datagen2026-10-12 12:01 UTC142
        (self.ident.etag_salt, version).hash(&mut hasher);
8b8e83329a1datagen2026-10-12 12:01 UTC143
        format!("\"{:016x}\"", hasher.finish())
8b8e83329a1datagen2026-10-12 12:01 UTC144
    }
8b8e83329a1datagen2026-10-12 12:01 UTC145
8b8e83329a1datagen2026-10-12 12:01 UTC146
    pub fn etag(&self) -> String {
8b8e83329a1datagen2026-10-12 12:01 UTC147
        self.etag_at(self.ident.version)
8b8e83329a1datagen2026-10-12 12:01 UTC148
    }
8b8e83329a1datagen2026-10-12 12:01 UTC149
8b8e83329a1datagen2026-10-12 12:01 UTC150
    /// The ETag the run had when it was created, which the true ending asks for.
8b8e83329a1datagen2026-10-12 12:01 UTC151
    pub fn origin_etag(&self) -> String {
8b8e83329a1datagen2026-10-12 12:01 UTC152
        self.etag_at(0)
8b8e83329a1datagen2026-10-12 12:01 UTC153
    }
8b8e83329a1datagen2026-10-12 12:01 UTC154
8b8e83329a1datagen2026-10-12 12:01 UTC155
    pub fn touch(&mut self) {
8b8e83329a1datagen2026-10-12 12:01 UTC156
        self.ident.version += 1;
8b8e83329a1datagen2026-10-12 12:01 UTC157
    }
8b8e83329a1datagen2026-10-12 12:01 UTC158
3ac624339c98atagen2026-10-13 12:21 UTC159
    pub(super) fn begin_turn(&mut self) -> Result<(), Problem> {
6033b0db186catagen2026-10-13 12:44 UTC160
        self.check_clock()?;
8b8e83329a1datagen2026-10-12 12:01 UTC161
        self.touch();
8b8e83329a1datagen2026-10-12 12:01 UTC162
        self.turn += 1;
8b8e83329a1datagen2026-10-12 12:01 UTC163
        self.here.turns += 1;
3ac624339c98atagen2026-10-13 12:21 UTC164
        self.body.torn = 0.0;
8b8e83329a1datagen2026-10-12 12:01 UTC165
        self.standing.at_turn_start = self.standing.suspicion;
6033b0db186catagen2026-10-13 12:44 UTC166
        self.realise(false);
3ac624339c98atagen2026-10-13 12:21 UTC167
        if self.floor.timed() && self.here.turns + 2 == FLOOR_LIMIT {
3ac624339c98atagen2026-10-13 12:21 UTC168
            self.notices.push(Notice::RunningLong(self.floor));
3ac624339c98atagen2026-10-13 12:21 UTC169
        } else if self.floor.timed() && self.here.turns + 1 == FLOOR_LIMIT {
3ac624339c98atagen2026-10-13 12:21 UTC170
            self.notices.push(Notice::LastTurn);
3ac624339c98atagen2026-10-13 12:21 UTC171
        }
3ac624339c98atagen2026-10-13 12:21 UTC172
        Ok(())
8b8e83329a1datagen2026-10-12 12:01 UTC173
    }
8b8e83329a1datagen2026-10-12 12:01 UTC174
8b8e83329a1datagen2026-10-12 12:01 UTC175
    pub(super) fn shed(&mut self, friction: i32) {
8b8e83329a1datagen2026-10-12 12:01 UTC176
        self.body.friction = (self.body.friction - friction).max(0);
8b8e83329a1datagen2026-10-12 12:01 UTC177
    }
8b8e83329a1datagen2026-10-12 12:01 UTC178
8b8e83329a1datagen2026-10-12 12:01 UTC179
    /// Priapism holds EQ up no matter what else moves it.
8b8e83329a1datagen2026-10-12 12:01 UTC180
    pub(super) fn reset_eq(&mut self, eq: i32) {
8b8e83329a1datagen2026-10-12 12:01 UTC181
        let floor = if self.body.conditions.priapism_turns > 0 {
8b8e83329a1datagen2026-10-12 12:01 UTC182
            PRIAPISM_EQ
8b8e83329a1datagen2026-10-12 12:01 UTC183
        } else {
8b8e83329a1datagen2026-10-12 12:01 UTC184
            0
8b8e83329a1datagen2026-10-12 12:01 UTC185
        };
8b8e83329a1datagen2026-10-12 12:01 UTC186
        self.body.eq = eq.max(floor);
8b8e83329a1datagen2026-10-12 12:01 UTC187
    }
8b8e83329a1datagen2026-10-12 12:01 UTC188
8b8e83329a1datagen2026-10-12 12:01 UTC189
    pub(super) fn end_kitchen_visit(&mut self) {
8b8e83329a1datagen2026-10-12 12:01 UTC190
        self.here.kitchen.melon_offered = false;
8b8e83329a1datagen2026-10-12 12:01 UTC191
    }
8b8e83329a1datagen2026-10-12 12:01 UTC192
8b8e83329a1datagen2026-10-12 12:01 UTC193
    /// Going back to the jelq walks away from both the fridge and Pump-Bro.
8b8e83329a1datagen2026-10-12 12:01 UTC194
    pub(super) fn step_away(&mut self) {
8b8e83329a1datagen2026-10-12 12:01 UTC195
        self.end_kitchen_visit();
8b8e83329a1datagen2026-10-12 12:01 UTC196
        self.here.shopping = false;
8b8e83329a1datagen2026-10-12 12:01 UTC197
    }
8b8e83329a1datagen2026-10-12 12:01 UTC198
8b8e83329a1datagen2026-10-12 12:01 UTC199
    pub(super) fn fresh_nonce(&mut self) -> String {
8b8e83329a1datagen2026-10-12 12:01 UTC200
        format!("{:016x}", self.oracle.u64())
8b8e83329a1datagen2026-10-12 12:01 UTC201
    }
8b8e83329a1datagen2026-10-12 12:01 UTC202
8b8e83329a1datagen2026-10-12 12:01 UTC203
    /// One game decision. The dice ignore the state prose, so it is only written for the model.
8b8e83329a1datagen2026-10-12 12:01 UTC204
    pub(super) fn decision(&self, site: &'static str, ask: &'static str) -> Roll {
8b8e83329a1datagen2026-10-12 12:01 UTC205
        Roll {
8b8e83329a1datagen2026-10-12 12:01 UTC206
            state: if self.oracle.listens() {
8b8e83329a1datagen2026-10-12 12:01 UTC207
                self.oracle_state()
8b8e83329a1datagen2026-10-12 12:01 UTC208
            } else {
8b8e83329a1datagen2026-10-12 12:01 UTC209
                String::new()
8b8e83329a1datagen2026-10-12 12:01 UTC210
            },
8b8e83329a1datagen2026-10-12 12:01 UTC211
            site,
8b8e83329a1datagen2026-10-12 12:01 UTC212
            ask,
8b8e83329a1datagen2026-10-12 12:01 UTC213
        }
8b8e83329a1datagen2026-10-12 12:01 UTC214
    }
8b8e83329a1datagen2026-10-12 12:01 UTC215
8b8e83329a1datagen2026-10-12 12:01 UTC216
    fn oracle_state(&self) -> String {
8b8e83329a1datagen2026-10-12 12:01 UTC217
        let mut s = format!(
8b8e83329a1datagen2026-10-12 12:01 UTC218
            "Floor {}, {}. Turn {}.\n\neq: {}\nstamina: {}\nfriction: {}\n",
8b8e83329a1datagen2026-10-12 12:01 UTC219
            self.floor.number(),
8b8e83329a1datagen2026-10-12 12:01 UTC220
            self.floor.place(),
8b8e83329a1datagen2026-10-12 12:01 UTC221
            self.turn,
8b8e83329a1datagen2026-10-12 12:01 UTC222
            self.body.eq,
8b8e83329a1datagen2026-10-12 12:01 UTC223
            self.body.stamina,
8b8e83329a1datagen2026-10-12 12:01 UTC224
            self.body.friction
8b8e83329a1datagen2026-10-12 12:01 UTC225
        );
8b8e83329a1datagen2026-10-12 12:01 UTC226
        let conditions = &self.body.conditions;
8b8e83329a1datagen2026-10-12 12:01 UTC227
        for (active, line) in [
8b8e83329a1datagen2026-10-12 12:01 UTC228
            (conditions.friction_burn, "friction burn: yes, untreated\n"),
8b8e83329a1datagen2026-10-12 12:01 UTC229
            (conditions.priapism_turns > 0, "priapism: yes\n"),
8b8e83329a1datagen2026-10-12 12:01 UTC230
            (conditions.peyronie, "peyronie's disease: yes\n"),
8b8e83329a1datagen2026-10-12 12:01 UTC231
            (conditions.pulled_muscle, "pulled muscle: yes\n"),
8b8e83329a1datagen2026-10-12 12:01 UTC232
        ] {
8b8e83329a1datagen2026-10-12 12:01 UTC233
            if active {
8b8e83329a1datagen2026-10-12 12:01 UTC234
                s.push_str(line);
8b8e83329a1datagen2026-10-12 12:01 UTC235
            }
8b8e83329a1datagen2026-10-12 12:01 UTC236
        }
8b8e83329a1datagen2026-10-12 12:01 UTC237
        s.push_str(&format!(
bf5f8cb6f60fatagen2026-10-13 12:10 UTC238
            "suspicion: {}\nshame: {}\ngrip: {}\n",
8b8e83329a1datagen2026-10-12 12:01 UTC239
            self.standing.suspicion,
8b8e83329a1datagen2026-10-12 12:01 UTC240
            self.standing.shame,
8b8e83329a1datagen2026-10-12 12:01 UTC241
            self.body.grip.name(),
8b8e83329a1datagen2026-10-12 12:01 UTC242
        ));
8b8e83329a1datagen2026-10-12 12:01 UTC243
        let held: Vec<&str> = self.items.owned().iter().map(|item| item.slug()).collect();
8b8e83329a1datagen2026-10-12 12:01 UTC244
        if !held.is_empty() {
8b8e83329a1datagen2026-10-12 12:01 UTC245
            s.push_str(&format!("items: {}\n", held.join(", ")));
8b8e83329a1datagen2026-10-12 12:01 UTC246
        }
8b8e83329a1datagen2026-10-12 12:01 UTC247
        if self.household.mum.is_some() {
8b8e83329a1datagen2026-10-12 12:01 UTC248
            s.push_str("Mum is at the door\n");
8b8e83329a1datagen2026-10-12 12:01 UTC249
        }
8b8e83329a1datagen2026-10-12 12:01 UTC250
        s
8b8e83329a1datagen2026-10-12 12:01 UTC251
    }
8b8e83329a1datagen2026-10-12 12:01 UTC252
8b8e83329a1datagen2026-10-12 12:01 UTC253
    pub(super) fn chance(&mut self, roll: Roll, odds: u32) -> bool {
8b8e83329a1datagen2026-10-12 12:01 UTC254
        self.oracle.chance(roll, odds)
8b8e83329a1datagen2026-10-12 12:01 UTC255
    }
8b8e83329a1datagen2026-10-12 12:01 UTC256
8b8e83329a1datagen2026-10-12 12:01 UTC257
    pub fn path(&self, sub: &str) -> String {
8b8e83329a1datagen2026-10-12 12:01 UTC258
        format!("/runs/{}{sub}", self.ident.id)
8b8e83329a1datagen2026-10-12 12:01 UTC259
    }
8b8e83329a1datagen2026-10-12 12:01 UTC260
8b8e83329a1datagen2026-10-12 12:01 UTC261
    pub fn current_length(&self) -> f64 {
8b8e83329a1datagen2026-10-12 12:01 UTC262
        self.body.length + self.here.temp_length
8b8e83329a1datagen2026-10-12 12:01 UTC263
    }
8b8e83329a1datagen2026-10-12 12:01 UTC264
8b8e83329a1datagen2026-10-12 12:01 UTC265
    pub fn self_reported(&self) -> f64 {
9adb509e6685atagen2026-10-14 08:54 UTC266
        self.current_length() + self.body.inflation
8b8e83329a1datagen2026-10-12 12:01 UTC267
    }
8b8e83329a1datagen2026-10-12 12:01 UTC268
8b8e83329a1datagen2026-10-12 12:01 UTC269
    pub fn shown_length(&self) -> f64 {
8b8e83329a1datagen2026-10-12 12:01 UTC270
        if self.items.kit.ruler {
8b8e83329a1datagen2026-10-12 12:01 UTC271
            self.current_length()
8b8e83329a1datagen2026-10-12 12:01 UTC272
        } else {
8b8e83329a1datagen2026-10-12 12:01 UTC273
            self.self_reported()
8b8e83329a1datagen2026-10-12 12:01 UTC274
        }
8b8e83329a1datagen2026-10-12 12:01 UTC275
    }
8b8e83329a1datagen2026-10-12 12:01 UTC276
8b8e83329a1datagen2026-10-12 12:01 UTC277
    pub fn max_stamina(&self) -> i32 {
8b8e83329a1datagen2026-10-12 12:01 UTC278
        if self.body.conditions.pulled_muscle {
8b8e83329a1datagen2026-10-12 12:01 UTC279
            PULLED_STAMINA
8b8e83329a1datagen2026-10-12 12:01 UTC280
        } else {
8b8e83329a1datagen2026-10-12 12:01 UTC281
            MAX_STAMINA
8b8e83329a1datagen2026-10-12 12:01 UTC282
        }
8b8e83329a1datagen2026-10-12 12:01 UTC283
    }
8b8e83329a1datagen2026-10-12 12:01 UTC284
8b8e83329a1datagen2026-10-12 12:01 UTC285
    pub fn forum_read(&self) -> bool {
8b8e83329a1datagen2026-10-12 12:01 UTC286
        self.online.story == Story::ReadForum
8b8e83329a1datagen2026-10-12 12:01 UTC287
    }
8b8e83329a1datagen2026-10-12 12:01 UTC288
8b8e83329a1datagen2026-10-12 12:01 UTC289
    pub fn thread_reachable(&self) -> bool {
8b8e83329a1datagen2026-10-12 12:01 UTC290
        self.turn >= BRANCH_TURN
8b8e83329a1datagen2026-10-12 12:01 UTC291
            || self.forum_read()
8b8e83329a1datagen2026-10-12 12:01 UTC292
            || self.floor.number() >= Floor::Forum.number()
8b8e83329a1datagen2026-10-12 12:01 UTC293
    }
8b8e83329a1datagen2026-10-12 12:01 UTC294
8b8e83329a1datagen2026-10-12 12:01 UTC295
    pub fn footage(&self) -> Option<(SystemTime, u64)> {
8b8e83329a1datagen2026-10-12 12:01 UTC296
        let filmed = self.online.filmed_at?;
8b8e83329a1datagen2026-10-12 12:01 UTC297
        let age = filmed.elapsed().map_or(0, |d| d.as_secs());
8b8e83329a1datagen2026-10-12 12:01 UTC298
        Some((filmed, age))
8b8e83329a1datagen2026-10-12 12:01 UTC299
    }
8b8e83329a1datagen2026-10-12 12:01 UTC300
8b8e83329a1datagen2026-10-12 12:01 UTC301
    pub fn status(&self) -> RunStatus {
8b8e83329a1datagen2026-10-12 12:01 UTC302
        if self.ended.is_some() {
8b8e83329a1datagen2026-10-12 12:01 UTC303
            RunStatus::Gone
8b8e83329a1datagen2026-10-12 12:01 UTC304
        } else if self.clinic.cured {
8b8e83329a1datagen2026-10-12 12:01 UTC305
            RunStatus::Normal
8b8e83329a1datagen2026-10-12 12:01 UTC306
        } else if self.clinic.prescribed {
8b8e83329a1datagen2026-10-12 12:01 UTC307
            RunStatus::Prescribed
8b8e83329a1datagen2026-10-12 12:01 UTC308
        } else {
8b8e83329a1datagen2026-10-12 12:01 UTC309
            RunStatus::Alive
8b8e83329a1datagen2026-10-12 12:01 UTC310
        }
8b8e83329a1datagen2026-10-12 12:01 UTC311
    }
8b8e83329a1datagen2026-10-12 12:01 UTC312
8b8e83329a1datagen2026-10-12 12:01 UTC313
    /// The parts of the run that its response headers do not already carry.
8b8e83329a1datagen2026-10-12 12:01 UTC314
    pub fn view(&self) -> Value {
8b8e83329a1datagen2026-10-12 12:01 UTC315
        let conditions: Vec<&str> = [
8b8e83329a1datagen2026-10-12 12:01 UTC316
            (self.here.teapot, "short and stout"),
8b8e83329a1datagen2026-10-12 12:01 UTC317
            (self.body.conditions.peyronie, "curved"),
8b8e83329a1datagen2026-10-12 12:01 UTC318
            (self.items.kit.cage, "caged"),
8b8e83329a1datagen2026-10-12 12:01 UTC319
            (self.body.conditions.priapism_turns > 0, "priapism"),
8b8e83329a1datagen2026-10-12 12:01 UTC320
            (self.body.conditions.pulled_muscle, "pulled muscle"),
8b8e83329a1datagen2026-10-12 12:01 UTC321
            (self.online.filmed_at.is_some(), "viral"),
8b8e83329a1datagen2026-10-12 12:01 UTC322
        ]
8b8e83329a1datagen2026-10-12 12:01 UTC323
        .into_iter()
8b8e83329a1datagen2026-10-12 12:01 UTC324
        .filter_map(|(active, name)| active.then_some(name))
8b8e83329a1datagen2026-10-12 12:01 UTC325
        .collect();
8b8e83329a1datagen2026-10-12 12:01 UTC326
        json!({
8b8e83329a1datagen2026-10-12 12:01 UTC327
            "public_id": self.ident.public_id,
8b8e83329a1datagen2026-10-12 12:01 UTC328
            "status": self.status().name(),
8b8e83329a1datagen2026-10-12 12:01 UTC329
            "turn": self.turn,
8b8e83329a1datagen2026-10-12 12:01 UTC330
            "run_shame": self.standing.shame,
8b8e83329a1datagen2026-10-12 12:01 UTC331
            "grip": self.body.grip.name(),
8b8e83329a1datagen2026-10-12 12:01 UTC332
            "ranked": !self.ident.seeded,
8b8e83329a1datagen2026-10-12 12:01 UTC333
            "conditions": conditions,
8b8e83329a1datagen2026-10-12 12:01 UTC334
        })
8b8e83329a1datagen2026-10-12 12:01 UTC335
    }
8b8e83329a1datagen2026-10-12 12:01 UTC336
8b8e83329a1datagen2026-10-12 12:01 UTC337
    pub fn end(&mut self, cause: Cause) -> Problem {
8b8e83329a1datagen2026-10-12 12:01 UTC338
        self.ended = Some(Ending {
8b8e83329a1datagen2026-10-12 12:01 UTC339
            cause,
8b8e83329a1datagen2026-10-12 12:01 UTC340
            turn: self.turn,
8b8e83329a1datagen2026-10-12 12:01 UTC341
        });
8b8e83329a1datagen2026-10-12 12:01 UTC342
        cause.problem().with("turn", self.turn)
8b8e83329a1datagen2026-10-12 12:01 UTC343
    }
8b8e83329a1datagen2026-10-12 12:01 UTC344
f48daa8adf59atagen2026-10-13 12:51 UTC345
    /// The refractory period is the next HOLD. Returns whether it rolled the second wind that refills stamina outright.
f48daa8adf59atagen2026-10-13 12:51 UTC346
    pub(super) fn recover(&mut self) -> bool {
8b8e83329a1datagen2026-10-12 12:01 UTC347
        self.body.exhausted = false;
f48daa8adf59atagen2026-10-13 12:51 UTC348
        let second_wind = self.chance(
f48daa8adf59atagen2026-10-13 12:51 UTC349
            self.decision(
f48daa8adf59atagen2026-10-13 12:51 UTC350
                "second-wind",
f48daa8adf59atagen2026-10-13 12:51 UTC351
                "Does the refractory period end in a second wind?",
f48daa8adf59atagen2026-10-13 12:51 UTC352
            ),
f48daa8adf59atagen2026-10-13 12:51 UTC353
            SECOND_WIND,
f48daa8adf59atagen2026-10-13 12:51 UTC354
        );
f48daa8adf59atagen2026-10-13 12:51 UTC355
        self.body.stamina = if second_wind {
f48daa8adf59atagen2026-10-13 12:51 UTC356
            self.max_stamina()
f48daa8adf59atagen2026-10-13 12:51 UTC357
        } else {
f48daa8adf59atagen2026-10-13 12:51 UTC358
            let (lo, hi) = REFRACTORY_STAMINA;
f48daa8adf59atagen2026-10-13 12:51 UTC359
            let back = self.oracle.span_int(
f48daa8adf59atagen2026-10-13 12:51 UTC360
                self.decision(
f48daa8adf59atagen2026-10-13 12:51 UTC361
                    "refractory",
f48daa8adf59atagen2026-10-13 12:51 UTC362
                    "How much stamina comes back after the refractory period?",
f48daa8adf59atagen2026-10-13 12:51 UTC363
                ),
f48daa8adf59atagen2026-10-13 12:51 UTC364
                lo,
f48daa8adf59atagen2026-10-13 12:51 UTC365
                hi,
f48daa8adf59atagen2026-10-13 12:51 UTC366
            );
f48daa8adf59atagen2026-10-13 12:51 UTC367
            back.min(self.max_stamina())
f48daa8adf59atagen2026-10-13 12:51 UTC368
        };
8b8e83329a1datagen2026-10-12 12:01 UTC369
        self.reset_eq(STARTING_EQ);
f48daa8adf59atagen2026-10-13 12:51 UTC370
        second_wind
8b8e83329a1datagen2026-10-12 12:01 UTC371
    }
8b8e83329a1datagen2026-10-12 12:01 UTC372
8b8e83329a1datagen2026-10-12 12:01 UTC373
    pub(super) fn check_exhausted(&self) -> Result<(), Problem> {
8b8e83329a1datagen2026-10-12 12:01 UTC374
        if !self.body.exhausted {
8b8e83329a1datagen2026-10-12 12:01 UTC375
            return Ok(());
8b8e83329a1datagen2026-10-12 12:01 UTC376
        }
8b8e83329a1datagen2026-10-12 12:01 UTC377
        Err(Problem::new(
8b8e83329a1datagen2026-10-12 12:01 UTC378
            StatusCode::TOO_MANY_REQUESTS,
8b8e83329a1datagen2026-10-12 12:01 UTC379
            "overtrained",
8b8e83329a1datagen2026-10-12 12:01 UTC380
            "Stamina is exhausted. HOLD for the refractory period.",
8b8e83329a1datagen2026-10-12 12:01 UTC381
        ))
8b8e83329a1datagen2026-10-12 12:01 UTC382
    }
8b8e83329a1datagen2026-10-12 12:01 UTC383
8b8e83329a1datagen2026-10-12 12:01 UTC384
    pub(super) fn exhaust(&mut self) {
8b8e83329a1datagen2026-10-12 12:01 UTC385
        self.body.stamina = 0;
8b8e83329a1datagen2026-10-12 12:01 UTC386
        self.body.exhausted = true;
8b8e83329a1datagen2026-10-12 12:01 UTC387
    }
8b8e83329a1datagen2026-10-12 12:01 UTC388
6033b0db186catagen2026-10-13 12:44 UTC389
    /// Leaving a floor lets time pass, so everything still queued arrives at once.
6033b0db186catagen2026-10-13 12:44 UTC390
    pub(super) fn realise(&mut self, everything: bool) {
8b8e83329a1datagen2026-10-12 12:01 UTC391
        let (due, waiting): (Vec<_>, Vec<_>) = std::mem::take(&mut self.body.pending)
8b8e83329a1datagen2026-10-12 12:01 UTC392
            .into_iter()
6033b0db186catagen2026-10-13 12:44 UTC393
            .partition(|p| everything || p.due <= self.turn);
8b8e83329a1datagen2026-10-12 12:01 UTC394
        self.body.pending = waiting;
8b8e83329a1datagen2026-10-12 12:01 UTC395
        for p in due {
fcc6cf6d5edcatagen2026-10-13 11:31 UTC396
            let foreskin = self.gain(p.length, p.girth, p.foreskin);
fcc6cf6d5edcatagen2026-10-13 11:31 UTC397
            let arrived = self.body.arrived.get_or_insert_default();
fcc6cf6d5edcatagen2026-10-13 11:31 UTC398
            arrived.length += p.length;
fcc6cf6d5edcatagen2026-10-13 11:31 UTC399
            arrived.girth += p.girth;
fcc6cf6d5edcatagen2026-10-13 11:31 UTC400
            if let Some(grown) = foreskin {
fcc6cf6d5edcatagen2026-10-13 11:31 UTC401
                *arrived.foreskin.get_or_insert(0.0) += grown;
fcc6cf6d5edcatagen2026-10-13 11:31 UTC402
            }
8b8e83329a1datagen2026-10-12 12:01 UTC403
        }
8b8e83329a1datagen2026-10-12 12:01 UTC404
    }
8b8e83329a1datagen2026-10-12 12:01 UTC405
3ac624339c98atagen2026-10-13 12:21 UTC406
    /// The foreskin change it returns nets out whatever friction tore off this turn.
97c05b741d8fatagen2026-10-13 11:27 UTC407
    pub(super) fn gain(&mut self, length: f64, girth: f64, foreskin: f64) -> Option<f64> {
8b8e83329a1datagen2026-10-12 12:01 UTC408
        self.body.length += length;
8b8e83329a1datagen2026-10-12 12:01 UTC409
        self.body.girth += girth;
3ac624339c98atagen2026-10-13 12:21 UTC410
        let torn = std::mem::take(&mut self.body.torn);
3ac624339c98atagen2026-10-13 12:21 UTC411
        let grown = match &mut self.body.foreskin {
3ac624339c98atagen2026-10-13 12:21 UTC412
            Some(f) => {
3ac624339c98atagen2026-10-13 12:21 UTC413
                *f += foreskin;
3ac624339c98atagen2026-10-13 12:21 UTC414
                foreskin
3ac624339c98atagen2026-10-13 12:21 UTC415
            }
3ac624339c98atagen2026-10-13 12:21 UTC416
            None => 0.0,
3ac624339c98atagen2026-10-13 12:21 UTC417
        };
3ac624339c98atagen2026-10-13 12:21 UTC418
        (grown != 0.0 || torn != 0.0).then_some(grown - torn)
8b8e83329a1datagen2026-10-12 12:01 UTC419
    }
8b8e83329a1datagen2026-10-12 12:01 UTC420
8b8e83329a1datagen2026-10-12 12:01 UTC421
    pub(super) fn rupture(&mut self) -> Problem {
8b8e83329a1datagen2026-10-12 12:01 UTC422
        let cause = Cause::Ruptured {
8b8e83329a1datagen2026-10-12 12:01 UTC423
            ice_unused: self.items.stores.ice_packs > 0,
8b8e83329a1datagen2026-10-12 12:01 UTC424
        };
8b8e83329a1datagen2026-10-12 12:01 UTC425
        self.end(cause)
8b8e83329a1datagen2026-10-12 12:01 UTC426
    }
8b8e83329a1datagen2026-10-12 12:01 UTC427
8b8e83329a1datagen2026-10-12 12:01 UTC428
    /// A run whose lock was poisoned by a panic mid-turn cannot be trusted to carry on.
8b8e83329a1datagen2026-10-12 12:01 UTC429
    pub fn lose(&mut self) {
8b8e83329a1datagen2026-10-12 12:01 UTC430
        if self.ended.is_none() {
8b8e83329a1datagen2026-10-12 12:01 UTC431
            self.end(Cause::Lost);
8b8e83329a1datagen2026-10-12 12:01 UTC432
        }
8b8e83329a1datagen2026-10-12 12:01 UTC433
    }
8b8e83329a1datagen2026-10-12 12:01 UTC434
}