atagen

jelq

public

an http based roguelike rpg about jelqing

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

Blame: src/game/mod.rs

782069860236atagen2026-10-10 12:44 UTC1
pub mod balance;
782069860236atagen2026-10-10 12:44 UTC2
mod body;
782069860236atagen2026-10-10 12:44 UTC3
mod clinic;
782069860236atagen2026-10-10 12:44 UTC4
mod floors;
782069860236atagen2026-10-10 12:44 UTC5
mod forum;
782069860236atagen2026-10-10 12:44 UTC6
mod hazards;
782069860236atagen2026-10-10 12:44 UTC7
mod household;
782069860236atagen2026-10-10 12:44 UTC8
mod items;
0a0bfee8329eatagen2026-10-11 05:11 UTC9
mod kitchen;
782069860236atagen2026-10-10 12:44 UTC10
mod outcome;
782069860236atagen2026-10-10 12:44 UTC11
mod shop;
782069860236atagen2026-10-10 12:44 UTC12
mod state;
782069860236atagen2026-10-10 12:44 UTC13
mod techniques;
782069860236atagen2026-10-10 12:44 UTC14
mod turn;
782069860236atagen2026-10-10 12:44 UTC15
mod utility;
782069860236atagen2026-10-10 12:44 UTC16
782069860236atagen2026-10-10 12:44 UTC17
pub use body::PatchOp;
782069860236atagen2026-10-10 12:44 UTC18
pub use forum::{ELDER_LAST_MODIFIED, FORUM_THREAD, forum_thread};
7d3fb63208bfatagen2026-10-11 04:38 UTC19
pub use items::{Item, Items, SHOP};
782069860236atagen2026-10-10 12:44 UTC20
pub use outcome::{Outcome, Payload};
782069860236atagen2026-10-10 12:44 UTC21
pub use state::{
9683736a2c5eatagen2026-10-10 13:15 UTC22
    Cause, Clinic, Conditions, Elder, Ending, Floor, FloorState, Grip, MumVisit, Observer, Story,
782069860236atagen2026-10-10 12:44 UTC23
};
782069860236atagen2026-10-10 12:44 UTC24
pub use techniques::{LUBRICANTS, Lube, Technique};
dfa38cf4cd4batagen2026-10-10 09:17 UTC25
99524cf9f17catagen2026-10-10 10:53 UTC26
use std::{
782069860236atagen2026-10-10 12:44 UTC27
    hash::{DefaultHasher, Hash, Hasher},
c3e0c27cf8fdatagen2026-10-10 11:28 UTC28
    time::{Duration, Instant, SystemTime},
99524cf9f17catagen2026-10-10 10:53 UTC29
};
dfa38cf4cd4batagen2026-10-10 09:17 UTC30
782069860236atagen2026-10-10 12:44 UTC31
use axum::http::{StatusCode, header};
dfa38cf4cd4batagen2026-10-10 09:17 UTC32
use rand::{Rng, SeedableRng, rngs::StdRng};
dfa38cf4cd4batagen2026-10-10 09:17 UTC33
use serde_json::{Value, json};
dfa38cf4cd4batagen2026-10-10 09:17 UTC34
dfa38cf4cd4batagen2026-10-10 09:17 UTC35
use crate::problem::Problem;
782069860236atagen2026-10-10 12:44 UTC36
use balance::{
14de3e1e8dc2atagen2026-10-11 06:19 UTC37
    HONEST_TOLERANCE, MAX_STAMINA, PRIAPISM_EQ, PULLED_STAMINA, SELF_REPORT_INFLATION,
14de3e1e8dc2atagen2026-10-11 06:19 UTC38
    STARTING_CASH, STARTING_EQ,
782069860236atagen2026-10-10 12:44 UTC39
};
782069860236atagen2026-10-10 12:44 UTC40
use state::Pending;
dfa38cf4cd4batagen2026-10-10 09:17 UTC41
dfa38cf4cd4batagen2026-10-10 09:17 UTC42
pub struct Run {
9683736a2c5eatagen2026-10-10 13:15 UTC43
    /// The private id in the run's URL. Knowing it is the only authorisation a run has.
dfa38cf4cd4batagen2026-10-10 09:17 UTC44
    pub id: String,
782069860236atagen2026-10-10 12:44 UTC45
    pub public_id: String,
782069860236atagen2026-10-10 12:44 UTC46
    etag_salt: u64,
dfa38cf4cd4batagen2026-10-10 09:17 UTC47
    pub version: u64,
dfa38cf4cd4batagen2026-10-10 09:17 UTC48
    pub turn: u32,
dfa38cf4cd4batagen2026-10-10 09:17 UTC49
    pub floor: Floor,
782069860236atagen2026-10-10 12:44 UTC50
    pub here: FloorState,
dfa38cf4cd4batagen2026-10-10 09:17 UTC51
    pub length: f64,
dfa38cf4cd4batagen2026-10-10 09:17 UTC52
    pub girth: f64,
dfa38cf4cd4batagen2026-10-10 09:17 UTC53
    pub foreskin: Option<f64>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC54
    pub eq: i32,
dfa38cf4cd4batagen2026-10-10 09:17 UTC55
    pub stamina: i32,
dfa38cf4cd4batagen2026-10-10 09:17 UTC56
    pub suspicion: u8,
dfa38cf4cd4batagen2026-10-10 09:17 UTC57
    pub shame: u32,
782069860236atagen2026-10-10 12:44 UTC58
    pub shame_reported: u32,
782069860236atagen2026-10-10 12:44 UTC59
    pub shame_total: u32,
dfa38cf4cd4batagen2026-10-10 09:17 UTC60
    pub cash: u32,
dfa38cf4cd4batagen2026-10-10 09:17 UTC61
    pub grip: Grip,
5dfebcc0ac43atagen2026-10-11 05:28 UTC62
    pub friction: i32,
5dfebcc0ac43atagen2026-10-11 05:28 UTC63
    pub jelq_grip: Option<Grip>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC64
    pub items: Items,
782069860236atagen2026-10-10 12:44 UTC65
    pub conditions: Conditions,
dfa38cf4cd4batagen2026-10-10 09:17 UTC66
    pub warmed_up: bool,
dfa38cf4cd4batagen2026-10-10 09:17 UTC67
    pub put_applied: bool,
782069860236atagen2026-10-10 12:44 UTC68
    pub story: Story,
0fdc47364c8catagen2026-10-11 04:45 UTC69
    pub meditation_taught: bool,
0a0bfee8329eatagen2026-10-11 05:11 UTC70
    pub kitchen_tellings: u8,
9683736a2c5eatagen2026-10-10 13:15 UTC71
    pub mum: Option<MumVisit>,
782069860236atagen2026-10-10 12:44 UTC72
    pub heard: Vec<String>,
782069860236atagen2026-10-10 12:44 UTC73
    pub elder: Elder,
782069860236atagen2026-10-10 12:44 UTC74
    pub clinic: Clinic,
99524cf9f17catagen2026-10-10 10:53 UTC75
    pub seeded: bool,
c3e0c27cf8fdatagen2026-10-10 11:28 UTC76
    pub filmed_at: Option<SystemTime>,
782069860236atagen2026-10-10 12:44 UTC77
    pub observed: Option<Observer>,
782069860236atagen2026-10-10 12:44 UTC78
    pub refractory: Duration,
dfa38cf4cd4batagen2026-10-10 09:17 UTC79
    pub refractory_until: Option<Instant>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC80
    pub pending: Vec<Pending>,
d46f9d0b953aatagen2026-10-10 14:41 UTC81
    pub notices: Vec<&'static str>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC82
    pub ended: Option<Ending>,
782069860236atagen2026-10-10 12:44 UTC83
    pub last_seen: Instant,
dfa38cf4cd4batagen2026-10-10 09:17 UTC84
    rng: StdRng,
dfa38cf4cd4batagen2026-10-10 09:17 UTC85
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC86
dfa38cf4cd4batagen2026-10-10 09:17 UTC87
pub fn round2(x: f64) -> f64 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC88
    (x * 100.0).round() / 100.0
dfa38cf4cd4batagen2026-10-10 09:17 UTC89
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC90
dfa38cf4cd4batagen2026-10-10 09:17 UTC91
fn round3(x: f64) -> f64 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC92
    (x * 1000.0).round() / 1000.0
dfa38cf4cd4batagen2026-10-10 09:17 UTC93
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC94
177535ac8abfatagen2026-10-10 10:39 UTC95
fn honest(claimed: f64, actual: f64) -> bool {
782069860236atagen2026-10-10 12:44 UTC96
    (claimed - actual).abs() < HONEST_TOLERANCE
dfa38cf4cd4batagen2026-10-10 09:17 UTC97
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC98
dfa38cf4cd4batagen2026-10-10 09:17 UTC99
fn not_extended() -> Problem {
dfa38cf4cd4batagen2026-10-10 09:17 UTC100
    Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC101
        StatusCode::NOT_EXTENDED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC102
        "not-extended",
dfa38cf4cd4batagen2026-10-10 09:17 UTC103
        "The requested extension was not applied. The server has recorded the attempt.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC104
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC105
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC106
dfa38cf4cd4batagen2026-10-10 09:17 UTC107
pub fn parse_credentials<'a>(authorization: &'a str, scheme: &str) -> Option<&'a str> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC108
    let (given, rest) = authorization.trim().split_once(' ')?;
dfa38cf4cd4batagen2026-10-10 09:17 UTC109
    given
dfa38cf4cd4batagen2026-10-10 09:17 UTC110
        .eq_ignore_ascii_case(scheme)
dfa38cf4cd4batagen2026-10-10 09:17 UTC111
        .then(|| rest.trim().trim_matches('"'))
dfa38cf4cd4batagen2026-10-10 09:17 UTC112
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC113
dfa38cf4cd4batagen2026-10-10 09:17 UTC114
pub fn etag_matches(condition: &str, etag: &str) -> bool {
dfa38cf4cd4batagen2026-10-10 09:17 UTC115
    condition
dfa38cf4cd4batagen2026-10-10 09:17 UTC116
        .split(',')
dfa38cf4cd4batagen2026-10-10 09:17 UTC117
        .map(str::trim)
dfa38cf4cd4batagen2026-10-10 09:17 UTC118
        .any(|tag| tag == "*" || tag.trim_start_matches("W/") == etag)
dfa38cf4cd4batagen2026-10-10 09:17 UTC119
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC120
dfa38cf4cd4batagen2026-10-10 09:17 UTC121
pub fn shaft(mm: usize) -> String {
dfa38cf4cd4batagen2026-10-10 09:17 UTC122
    format!("8{}D", "=".repeat(mm.saturating_sub(2)))
dfa38cf4cd4batagen2026-10-10 09:17 UTC123
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC124
dfa38cf4cd4batagen2026-10-10 09:17 UTC125
impl Run {
782069860236atagen2026-10-10 12:44 UTC126
    pub fn new(id: String, public_id: String, seed: u64) -> Self {
dfa38cf4cd4batagen2026-10-10 09:17 UTC127
        let mut rng = StdRng::seed_from_u64(seed);
dfa38cf4cd4batagen2026-10-10 09:17 UTC128
        let length = round2(rng.random_range(4.5..6.5));
dfa38cf4cd4batagen2026-10-10 09:17 UTC129
        let girth = round2(rng.random_range(4.0..5.0));
dfa38cf4cd4batagen2026-10-10 09:17 UTC130
        let foreskin = round2(rng.random_range(0.4..2.4));
782069860236atagen2026-10-10 12:44 UTC131
        let etag_salt = rng.random();
782069860236atagen2026-10-10 12:44 UTC132
        Self {
dfa38cf4cd4batagen2026-10-10 09:17 UTC133
            id,
782069860236atagen2026-10-10 12:44 UTC134
            public_id,
782069860236atagen2026-10-10 12:44 UTC135
            etag_salt,
dfa38cf4cd4batagen2026-10-10 09:17 UTC136
            version: 0,
dfa38cf4cd4batagen2026-10-10 09:17 UTC137
            turn: 0,
dfa38cf4cd4batagen2026-10-10 09:17 UTC138
            floor: Floor::Bedroom,
782069860236atagen2026-10-10 12:44 UTC139
            here: FloorState::default(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC140
            length,
dfa38cf4cd4batagen2026-10-10 09:17 UTC141
            girth,
dfa38cf4cd4batagen2026-10-10 09:17 UTC142
            foreskin: Some(foreskin),
782069860236atagen2026-10-10 12:44 UTC143
            eq: STARTING_EQ,
dfa38cf4cd4batagen2026-10-10 09:17 UTC144
            stamina: MAX_STAMINA,
dfa38cf4cd4batagen2026-10-10 09:17 UTC145
            suspicion: 0,
dfa38cf4cd4batagen2026-10-10 09:17 UTC146
            shame: 0,
782069860236atagen2026-10-10 12:44 UTC147
            shame_reported: 0,
782069860236atagen2026-10-10 12:44 UTC148
            shame_total: 0,
dfa38cf4cd4batagen2026-10-10 09:17 UTC149
            cash: STARTING_CASH,
dfa38cf4cd4batagen2026-10-10 09:17 UTC150
            grip: Grip::Overhand,
5dfebcc0ac43atagen2026-10-11 05:28 UTC151
            friction: 0,
5dfebcc0ac43atagen2026-10-11 05:28 UTC152
            jelq_grip: None,
782069860236atagen2026-10-10 12:44 UTC153
            items: Items::default(),
782069860236atagen2026-10-10 12:44 UTC154
            conditions: Conditions::default(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC155
            warmed_up: false,
dfa38cf4cd4batagen2026-10-10 09:17 UTC156
            put_applied: false,
782069860236atagen2026-10-10 12:44 UTC157
            story: Story::Unopened,
0fdc47364c8catagen2026-10-11 04:45 UTC158
            meditation_taught: false,
0a0bfee8329eatagen2026-10-11 05:11 UTC159
            kitchen_tellings: 0,
9683736a2c5eatagen2026-10-10 13:15 UTC160
            mum: None,
782069860236atagen2026-10-10 12:44 UTC161
            heard: Vec::new(),
d46f9d0b953aatagen2026-10-10 14:41 UTC162
            notices: Vec::new(),
782069860236atagen2026-10-10 12:44 UTC163
            elder: Elder::Unverified { strikes: 0 },
782069860236atagen2026-10-10 12:44 UTC164
            clinic: Clinic::default(),
99524cf9f17catagen2026-10-10 10:53 UTC165
            seeded: false,
c3e0c27cf8fdatagen2026-10-10 11:28 UTC166
            filmed_at: None,
dfa38cf4cd4batagen2026-10-10 09:17 UTC167
            observed: None,
782069860236atagen2026-10-10 12:44 UTC168
            refractory: Duration::from_secs(30),
dfa38cf4cd4batagen2026-10-10 09:17 UTC169
            refractory_until: None,
dfa38cf4cd4batagen2026-10-10 09:17 UTC170
            pending: Vec::new(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC171
            ended: None,
782069860236atagen2026-10-10 12:44 UTC172
            last_seen: Instant::now(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC173
            rng,
782069860236atagen2026-10-10 12:44 UTC174
        }
782069860236atagen2026-10-10 12:44 UTC175
    }
782069860236atagen2026-10-10 12:44 UTC176
782069860236atagen2026-10-10 12:44 UTC177
    fn etag_at(&self, version: u64) -> String {
782069860236atagen2026-10-10 12:44 UTC178
        let mut hasher = DefaultHasher::new();
782069860236atagen2026-10-10 12:44 UTC179
        (self.etag_salt, version).hash(&mut hasher);
782069860236atagen2026-10-10 12:44 UTC180
        format!("\"{:016x}\"", hasher.finish())
dfa38cf4cd4batagen2026-10-10 09:17 UTC181
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC182
dfa38cf4cd4batagen2026-10-10 09:17 UTC183
    pub fn etag(&self) -> String {
782069860236atagen2026-10-10 12:44 UTC184
        self.etag_at(self.version)
782069860236atagen2026-10-10 12:44 UTC185
    }
782069860236atagen2026-10-10 12:44 UTC186
782069860236atagen2026-10-10 12:44 UTC187
    /// The ETag the run had when it was created, which the true ending asks for.
782069860236atagen2026-10-10 12:44 UTC188
    pub fn origin_etag(&self) -> String {
782069860236atagen2026-10-10 12:44 UTC189
        self.etag_at(0)
dfa38cf4cd4batagen2026-10-10 09:17 UTC190
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC191
dfa38cf4cd4batagen2026-10-10 09:17 UTC192
    pub fn touch(&mut self) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC193
        self.version += 1;
dfa38cf4cd4batagen2026-10-10 09:17 UTC194
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC195
5dfebcc0ac43atagen2026-10-11 05:28 UTC196
    pub(super) fn shed(&mut self, friction: i32) {
5dfebcc0ac43atagen2026-10-11 05:28 UTC197
        self.friction = (self.friction - friction).max(0);
5dfebcc0ac43atagen2026-10-11 05:28 UTC198
    }
5dfebcc0ac43atagen2026-10-11 05:28 UTC199
14de3e1e8dc2atagen2026-10-11 06:19 UTC200
    /// Priapism holds EQ up no matter what else moves it.
14de3e1e8dc2atagen2026-10-11 06:19 UTC201
    pub(super) fn reset_eq(&mut self, eq: i32) {
14de3e1e8dc2atagen2026-10-11 06:19 UTC202
        let floor = if self.conditions.priapism_turns > 0 {
14de3e1e8dc2atagen2026-10-11 06:19 UTC203
            PRIAPISM_EQ
14de3e1e8dc2atagen2026-10-11 06:19 UTC204
        } else {
14de3e1e8dc2atagen2026-10-11 06:19 UTC205
            0
14de3e1e8dc2atagen2026-10-11 06:19 UTC206
        };
14de3e1e8dc2atagen2026-10-11 06:19 UTC207
        self.eq = eq.max(floor);
14de3e1e8dc2atagen2026-10-11 06:19 UTC208
    }
14de3e1e8dc2atagen2026-10-11 06:19 UTC209
dfa38cf4cd4batagen2026-10-10 09:17 UTC210
    fn fresh_nonce(&mut self) -> String {
dfa38cf4cd4batagen2026-10-10 09:17 UTC211
        format!("{:016x}", self.rng.random::<u64>())
dfa38cf4cd4batagen2026-10-10 09:17 UTC212
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC213
782069860236atagen2026-10-10 12:44 UTC214
    fn chance(&mut self, odds: u32) -> bool {
782069860236atagen2026-10-10 12:44 UTC215
        self.rng.random_ratio(1, odds)
782069860236atagen2026-10-10 12:44 UTC216
    }
782069860236atagen2026-10-10 12:44 UTC217
782069860236atagen2026-10-10 12:44 UTC218
    pub fn path(&self, sub: &str) -> String {
782069860236atagen2026-10-10 12:44 UTC219
        format!("/runs/{}{sub}", self.id)
782069860236atagen2026-10-10 12:44 UTC220
    }
782069860236atagen2026-10-10 12:44 UTC221
dfa38cf4cd4batagen2026-10-10 09:17 UTC222
    pub fn current_length(&self) -> f64 {
782069860236atagen2026-10-10 12:44 UTC223
        self.length + self.here.temp_length
dfa38cf4cd4batagen2026-10-10 09:17 UTC224
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC225
dfa38cf4cd4batagen2026-10-10 09:17 UTC226
    pub fn self_reported(&self) -> f64 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC227
        self.current_length() + SELF_REPORT_INFLATION
dfa38cf4cd4batagen2026-10-10 09:17 UTC228
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC229
c3e0c27cf8fdatagen2026-10-10 11:28 UTC230
    pub fn max_stamina(&self) -> i32 {
782069860236atagen2026-10-10 12:44 UTC231
        if self.conditions.pulled_muscle {
782069860236atagen2026-10-10 12:44 UTC232
            PULLED_STAMINA
782069860236atagen2026-10-10 12:44 UTC233
        } else {
782069860236atagen2026-10-10 12:44 UTC234
            MAX_STAMINA
782069860236atagen2026-10-10 12:44 UTC235
        }
782069860236atagen2026-10-10 12:44 UTC236
    }
782069860236atagen2026-10-10 12:44 UTC237
782069860236atagen2026-10-10 12:44 UTC238
    pub fn forum_read(&self) -> bool {
782069860236atagen2026-10-10 12:44 UTC239
        self.story == Story::ReadForum
c3e0c27cf8fdatagen2026-10-10 11:28 UTC240
    }
c3e0c27cf8fdatagen2026-10-10 11:28 UTC241
c3e0c27cf8fdatagen2026-10-10 11:28 UTC242
    pub fn footage(&self) -> Option<(SystemTime, u64)> {
c3e0c27cf8fdatagen2026-10-10 11:28 UTC243
        let filmed = self.filmed_at?;
c3e0c27cf8fdatagen2026-10-10 11:28 UTC244
        let age = filmed.elapsed().map_or(0, |d| d.as_secs());
c3e0c27cf8fdatagen2026-10-10 11:28 UTC245
        Some((filmed, age))
c3e0c27cf8fdatagen2026-10-10 11:28 UTC246
    }
c3e0c27cf8fdatagen2026-10-10 11:28 UTC247
dfa38cf4cd4batagen2026-10-10 09:17 UTC248
    pub fn status(&self) -> &'static str {
782069860236atagen2026-10-10 12:44 UTC249
        match (&self.ended, self.clinic.cured) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC250
            (Some(_), _) => "gone",
dfa38cf4cd4batagen2026-10-10 09:17 UTC251
            (None, true) => "normal",
dfa38cf4cd4batagen2026-10-10 09:17 UTC252
            (None, false) => "alive",
dfa38cf4cd4batagen2026-10-10 09:17 UTC253
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC254
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC255
782069860236atagen2026-10-10 12:44 UTC256
    /// The parts of the run that its response headers do not already carry.
dfa38cf4cd4batagen2026-10-10 09:17 UTC257
    pub fn view(&self) -> Value {
dfa38cf4cd4batagen2026-10-10 09:17 UTC258
        let conditions: Vec<&str> = [
782069860236atagen2026-10-10 12:44 UTC259
            (self.here.teapot, "short and stout"),
782069860236atagen2026-10-10 12:44 UTC260
            (self.conditions.peyronie, "curved"),
dfa38cf4cd4batagen2026-10-10 09:17 UTC261
            (self.items.cage, "caged"),
782069860236atagen2026-10-10 12:44 UTC262
            (self.conditions.priapism_turns > 0, "priapism"),
782069860236atagen2026-10-10 12:44 UTC263
            (self.conditions.pulled_muscle, "pulled muscle"),
c3e0c27cf8fdatagen2026-10-10 11:28 UTC264
            (self.filmed_at.is_some(), "viral"),
dfa38cf4cd4batagen2026-10-10 09:17 UTC265
        ]
dfa38cf4cd4batagen2026-10-10 09:17 UTC266
        .into_iter()
dfa38cf4cd4batagen2026-10-10 09:17 UTC267
        .filter_map(|(active, name)| active.then_some(name))
dfa38cf4cd4batagen2026-10-10 09:17 UTC268
        .collect();
782069860236atagen2026-10-10 12:44 UTC269
        json!({
782069860236atagen2026-10-10 12:44 UTC270
            "public_id": self.public_id,
dfa38cf4cd4batagen2026-10-10 09:17 UTC271
            "status": self.status(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC272
            "turn": self.turn,
782069860236atagen2026-10-10 12:44 UTC273
            "run_shame": self.shame,
dfa38cf4cd4batagen2026-10-10 09:17 UTC274
            "grip": self.grip.name(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC275
            "warmed_up": self.warmed_up,
99524cf9f17catagen2026-10-10 10:53 UTC276
            "ranked": !self.seeded,
dfa38cf4cd4batagen2026-10-10 09:17 UTC277
            "conditions": conditions,
782069860236atagen2026-10-10 12:44 UTC278
        })
dfa38cf4cd4batagen2026-10-10 09:17 UTC279
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC280
782069860236atagen2026-10-10 12:44 UTC281
    pub fn end(&mut self, cause: Cause) -> Problem {
dfa38cf4cd4batagen2026-10-10 09:17 UTC282
        self.ended = Some(Ending {
dfa38cf4cd4batagen2026-10-10 09:17 UTC283
            cause,
dfa38cf4cd4batagen2026-10-10 09:17 UTC284
            turn: self.turn,
dfa38cf4cd4batagen2026-10-10 09:17 UTC285
        });
782069860236atagen2026-10-10 12:44 UTC286
        cause.problem().with("turn", self.turn)
dfa38cf4cd4batagen2026-10-10 09:17 UTC287
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC288
e76ab808e7adatagen2026-10-10 13:46 UTC289
    /// Ends the refractory period once it has run out, whatever the request.
e76ab808e7adatagen2026-10-10 13:46 UTC290
    pub fn recover(&mut self, now: Instant) {
e76ab808e7adatagen2026-10-10 13:46 UTC291
        if self.refractory_until.is_some_and(|until| now >= until) {
e76ab808e7adatagen2026-10-10 13:46 UTC292
            self.refractory_until = None;
e76ab808e7adatagen2026-10-10 13:46 UTC293
            self.stamina = self.max_stamina();
14de3e1e8dc2atagen2026-10-11 06:19 UTC294
            self.reset_eq(STARTING_EQ);
e76ab808e7adatagen2026-10-10 13:46 UTC295
            self.warmed_up = false;
e76ab808e7adatagen2026-10-10 13:46 UTC296
            self.touch();
e76ab808e7adatagen2026-10-10 13:46 UTC297
        }
e76ab808e7adatagen2026-10-10 13:46 UTC298
    }
e76ab808e7adatagen2026-10-10 13:46 UTC299
dfa38cf4cd4batagen2026-10-10 09:17 UTC300
    fn check_refractory(&mut self, now: Instant) -> Result<(), Problem> {
e76ab808e7adatagen2026-10-10 13:46 UTC301
        self.recover(now);
dfa38cf4cd4batagen2026-10-10 09:17 UTC302
        let Some(until) = self.refractory_until else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC303
            return Ok(());
dfa38cf4cd4batagen2026-10-10 09:17 UTC304
        };
e76ab808e7adatagen2026-10-10 13:46 UTC305
        let seconds = until.saturating_duration_since(now).as_secs() + 1;
e76ab808e7adatagen2026-10-10 13:46 UTC306
        Err(Problem::new(
e76ab808e7adatagen2026-10-10 13:46 UTC307
            StatusCode::TOO_MANY_REQUESTS,
e76ab808e7adatagen2026-10-10 13:46 UTC308
            "overtrained",
e76ab808e7adatagen2026-10-10 13:46 UTC309
            "Stamina is exhausted. The refractory period is in effect.",
e76ab808e7adatagen2026-10-10 13:46 UTC310
        )
e76ab808e7adatagen2026-10-10 13:46 UTC311
        .header(header::RETRY_AFTER, seconds.to_string()))
dfa38cf4cd4batagen2026-10-10 09:17 UTC312
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC313
782069860236atagen2026-10-10 12:44 UTC314
    fn exhaust(&mut self, now: Instant) {
782069860236atagen2026-10-10 12:44 UTC315
        self.stamina = 0;
782069860236atagen2026-10-10 12:44 UTC316
        self.refractory_until = Some(now + self.refractory);
782069860236atagen2026-10-10 12:44 UTC317
    }
782069860236atagen2026-10-10 12:44 UTC318
9683736a2c5eatagen2026-10-10 13:15 UTC319
    fn realise(&mut self) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC320
        let (due, waiting): (Vec<_>, Vec<_>) = std::mem::take(&mut self.pending)
dfa38cf4cd4batagen2026-10-10 09:17 UTC321
            .into_iter()
dfa38cf4cd4batagen2026-10-10 09:17 UTC322
            .partition(|p| p.due <= self.turn);
dfa38cf4cd4batagen2026-10-10 09:17 UTC323
        self.pending = waiting;
dfa38cf4cd4batagen2026-10-10 09:17 UTC324
        for p in due {
dfa38cf4cd4batagen2026-10-10 09:17 UTC325
            self.length += p.length;
dfa38cf4cd4batagen2026-10-10 09:17 UTC326
            self.girth += p.girth;
dfa38cf4cd4batagen2026-10-10 09:17 UTC327
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC328
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC329
dfa38cf4cd4batagen2026-10-10 09:17 UTC330
    fn gain(&mut self, length: f64, girth: f64, foreskin: f64) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC331
        self.length += length;
dfa38cf4cd4batagen2026-10-10 09:17 UTC332
        self.girth += girth;
dfa38cf4cd4batagen2026-10-10 09:17 UTC333
        if let Some(f) = &mut self.foreskin {
dfa38cf4cd4batagen2026-10-10 09:17 UTC334
            *f += foreskin;
dfa38cf4cd4batagen2026-10-10 09:17 UTC335
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC336
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC337
dfa38cf4cd4batagen2026-10-10 09:17 UTC338
    fn rupture(&mut self) -> Problem {
782069860236atagen2026-10-10 12:44 UTC339
        let cause = Cause::Ruptured {
782069860236atagen2026-10-10 12:44 UTC340
            ice_unused: self.items.ice_packs > 0,
dfa38cf4cd4batagen2026-10-10 09:17 UTC341
        };
782069860236atagen2026-10-10 12:44 UTC342
        self.end(cause)
dfa38cf4cd4batagen2026-10-10 09:17 UTC343
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC344
}