atagen

jelq

public

an http based roguelike rpg about jelqing

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

Blame: src/game/hazards.rs

782069860236atagen2026-10-10 12:44 UTC1
use std::{ops::ControlFlow, time::SystemTime};
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::{
8b8e83329a1datagen2026-10-12 12:01 UTC7
    Cause, Floor, Notice, Outcome, Run,
782069860236atagen2026-10-10 12:44 UTC8
    balance::{
4b1f5f88827datagen2026-10-13 10:18 UTC9
        BURN_RUPTURE, COLD_DEGLOVING, COLD_EQ, CURVE_MISSES, DANGER_EQ, DEGLOVING,
4b1f5f88827datagen2026-10-13 10:18 UTC10
        DEGLOVING_INTENSITY, EQ_BAND, FRIEND, HOT_EQ, LOST_IT_EQ, PREMATURE_EQ, RUPTURE_HOT,
4b1f5f88827datagen2026-10-13 10:18 UTC11
        RUPTURE_IN_BAND, RUPTURE_PREMATURE, RUPTURE_WARM, SOFT_INTENSITY, SUSPICION_LIMIT,
4b1f5f88827datagen2026-10-13 10:18 UTC12
        WANDERER_VISIT, WINDOW,
782069860236atagen2026-10-10 12:44 UTC13
    },
782069860236atagen2026-10-10 12:44 UTC14
    techniques::Training,
8b8e83329a1datagen2026-10-12 12:01 UTC15
    travel::Exit,
782069860236atagen2026-10-10 12:44 UTC16
};
782069860236atagen2026-10-10 12:44 UTC17
use crate::problem::Problem;
782069860236atagen2026-10-10 12:44 UTC18
782069860236atagen2026-10-10 12:44 UTC19
type Interruption = Result<ControlFlow<Outcome>, Problem>;
782069860236atagen2026-10-10 12:44 UTC20
782069860236atagen2026-10-10 12:44 UTC21
impl Run {
8b8e83329a1datagen2026-10-12 12:01 UTC22
    /// Whether EQ is past `line`, with a friction burn dragging every line down.
8b8e83329a1datagen2026-10-12 12:01 UTC23
    pub(super) fn hotter_than(&self, line: i32) -> bool {
8b8e83329a1datagen2026-10-12 12:01 UTC24
        let burn = if self.body.conditions.friction_burn {
b85a0d4536c3atagen2026-10-11 08:50 UTC25
            BURN_RUPTURE
b85a0d4536c3atagen2026-10-11 08:50 UTC26
        } else {
b85a0d4536c3atagen2026-10-11 08:50 UTC27
            0
8b8e83329a1datagen2026-10-12 12:01 UTC28
        };
8b8e83329a1datagen2026-10-12 12:01 UTC29
        self.body.eq > line - burn
b85a0d4536c3atagen2026-10-11 08:50 UTC30
    }
b85a0d4536c3atagen2026-10-11 08:50 UTC31
b85a0d4536c3atagen2026-10-11 08:50 UTC32
    /// The rupture tier for any turn that is not a jelq, so never the premature band's 1 in 3.
b85a0d4536c3atagen2026-10-11 08:50 UTC33
    pub(super) fn rupture_odds(&self) -> u32 {
8b8e83329a1datagen2026-10-12 12:01 UTC34
        if !self.hotter_than(DANGER_EQ) {
b85a0d4536c3atagen2026-10-11 08:50 UTC35
            RUPTURE_IN_BAND
8b8e83329a1datagen2026-10-12 12:01 UTC36
        } else if !self.hotter_than(HOT_EQ) {
b85a0d4536c3atagen2026-10-11 08:50 UTC37
            RUPTURE_WARM
b85a0d4536c3atagen2026-10-11 08:50 UTC38
        } else {
b85a0d4536c3atagen2026-10-11 08:50 UTC39
            RUPTURE_HOT
b85a0d4536c3atagen2026-10-11 08:50 UTC40
        }
b85a0d4536c3atagen2026-10-11 08:50 UTC41
    }
b85a0d4536c3atagen2026-10-11 08:50 UTC42
782069860236atagen2026-10-10 12:44 UTC43
    pub(super) fn mishaps(&mut self, training: Training, intensity: i32) -> Interruption {
4b1f5f88827datagen2026-10-13 10:18 UTC44
        if !self.body.warmed_up {
4b1f5f88827datagen2026-10-13 10:18 UTC45
            if !self.notices.contains(&Notice::ColdStart) {
4b1f5f88827datagen2026-10-13 10:18 UTC46
                self.notices.push(Notice::ColdStart);
4b1f5f88827datagen2026-10-13 10:18 UTC47
            }
4b1f5f88827datagen2026-10-13 10:18 UTC48
            if self.chance(
4b1f5f88827datagen2026-10-13 10:18 UTC49
                self.decision("cold-degloving", "Cold and unprepared, does the skin tear?"),
4b1f5f88827datagen2026-10-13 10:18 UTC50
                COLD_DEGLOVING,
4b1f5f88827datagen2026-10-13 10:18 UTC51
            ) {
4b1f5f88827datagen2026-10-13 10:18 UTC52
                return Err(self.end(Cause::Degloved));
4b1f5f88827datagen2026-10-13 10:18 UTC53
            }
4b1f5f88827datagen2026-10-13 10:18 UTC54
        }
3f9091557ec3atagen2026-10-11 11:43 UTC55
        if training == Training::Dry
3f9091557ec3atagen2026-10-11 11:43 UTC56
            && intensity >= DEGLOVING_INTENSITY
3f9091557ec3atagen2026-10-11 11:43 UTC57
            && self.chance(
3f9091557ec3atagen2026-10-11 11:43 UTC58
                self.decision("degloving", "Dry at this intensity, does the skin tear?"),
3f9091557ec3atagen2026-10-11 11:43 UTC59
                DEGLOVING,
3f9091557ec3atagen2026-10-11 11:43 UTC60
            )
3f9091557ec3atagen2026-10-11 11:43 UTC61
        {
782069860236atagen2026-10-10 12:44 UTC62
            return Err(self.end(Cause::Degloved));
782069860236atagen2026-10-10 12:44 UTC63
        }
782069860236atagen2026-10-10 12:44 UTC64
        Ok(ControlFlow::Continue(()))
782069860236atagen2026-10-10 12:44 UTC65
    }
782069860236atagen2026-10-10 12:44 UTC66
782069860236atagen2026-10-10 12:44 UTC67
    pub(super) fn hazards(&mut self, intensity: i32) -> Interruption {
8b8e83329a1datagen2026-10-12 12:01 UTC68
        if !self.hotter_than(PREMATURE_EQ)
3f9091557ec3atagen2026-10-11 11:43 UTC69
            && self.chance(
3f9091557ec3atagen2026-10-11 11:43 UTC70
                self.decision("rupture", "Does this jelq rupture something?"),
3f9091557ec3atagen2026-10-11 11:43 UTC71
                self.rupture_odds(),
3f9091557ec3atagen2026-10-11 11:43 UTC72
            )
3f9091557ec3atagen2026-10-11 11:43 UTC73
        {
782069860236atagen2026-10-10 12:44 UTC74
            return Err(self.rupture());
782069860236atagen2026-10-10 12:44 UTC75
        }
3f9091557ec3atagen2026-10-11 11:43 UTC76
        if self.floor.has_window()
8b8e83329a1datagen2026-10-12 12:01 UTC77
            && !self.items.kit.blinds
3f9091557ec3atagen2026-10-11 11:43 UTC78
            && self.oracle.chance_not(
3f9091557ec3atagen2026-10-11 11:43 UTC79
                self.decision("window", "Are the curtains still closed?"),
3f9091557ec3atagen2026-10-11 11:43 UTC80
                WINDOW,
3f9091557ec3atagen2026-10-11 11:43 UTC81
            )
3f9091557ec3atagen2026-10-11 11:43 UTC82
        {
8b8e83329a1datagen2026-10-12 12:01 UTC83
            self.standing.shame += 30;
8b8e83329a1datagen2026-10-12 12:01 UTC84
            self.online.filmed_at.get_or_insert_with(SystemTime::now);
782069860236atagen2026-10-10 12:44 UTC85
            return Ok(ControlFlow::Break(
782069860236atagen2026-10-10 12:44 UTC86
                Outcome::json(
782069860236atagen2026-10-10 12:44 UTC87
                    StatusCode::CREATED,
782069860236atagen2026-10-10 12:44 UTC88
                    json!({ "detail": "A neighbour was at the window. A recording has been created. It has been shared." }),
782069860236atagen2026-10-10 12:44 UTC89
                )
782069860236atagen2026-10-10 12:44 UTC90
                .header(header::LOCATION, self.path("/footage")),
782069860236atagen2026-10-10 12:44 UTC91
            ));
782069860236atagen2026-10-10 12:44 UTC92
        }
3f9091557ec3atagen2026-10-11 11:43 UTC93
        if self.floor == Floor::ChangingRoom
3f9091557ec3atagen2026-10-11 11:43 UTC94
            && self.chance(
3f9091557ec3atagen2026-10-11 11:43 UTC95
                self.decision("friend", "Does a friend walk in and offer to help?"),
3f9091557ec3atagen2026-10-11 11:43 UTC96
                FRIEND,
3f9091557ec3atagen2026-10-11 11:43 UTC97
            )
3f9091557ec3atagen2026-10-11 11:43 UTC98
        {
8b8e83329a1datagen2026-10-12 12:01 UTC99
            self.standing.shame += 1;
782069860236atagen2026-10-10 12:44 UTC100
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC101
                StatusCode::USE_PROXY,
782069860236atagen2026-10-10 12:44 UTC102
                "friend",
782069860236atagen2026-10-10 12:44 UTC103
                "A friend has offered to help. Use Proxy is deprecated, and the server has declined on your behalf.",
782069860236atagen2026-10-10 12:44 UTC104
            ));
782069860236atagen2026-10-10 12:44 UTC105
        }
8b8e83329a1datagen2026-10-12 12:01 UTC106
        if self.body.conditions.peyronie
3f9091557ec3atagen2026-10-11 11:43 UTC107
            && self.chance(
3f9091557ec3atagen2026-10-11 11:43 UTC108
                self.decision(
3f9091557ec3atagen2026-10-11 11:43 UTC109
                    "misdirected",
3f9091557ec3atagen2026-10-11 11:43 UTC110
                    "With the curve, does the stroke land sharply to the left?",
3f9091557ec3atagen2026-10-11 11:43 UTC111
                ),
3f9091557ec3atagen2026-10-11 11:43 UTC112
                CURVE_MISSES,
3f9091557ec3atagen2026-10-11 11:43 UTC113
            )
3f9091557ec3atagen2026-10-11 11:43 UTC114
        {
782069860236atagen2026-10-10 12:44 UTC115
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC116
                StatusCode::MISDIRECTED_REQUEST,
782069860236atagen2026-10-10 12:44 UTC117
                "misdirected",
782069860236atagen2026-10-10 12:44 UTC118
                "The technique landed slightly to the left. It was meant for a different server.",
782069860236atagen2026-10-10 12:44 UTC119
            ));
782069860236atagen2026-10-10 12:44 UTC120
        }
8b8e83329a1datagen2026-10-12 12:01 UTC121
        if intensity <= SOFT_INTENSITY && self.body.eq < *EQ_BAND.start() {
8b8e83329a1datagen2026-10-12 12:01 UTC122
            self.body.eq = (self.body.eq - LOST_IT_EQ).max(0);
782069860236atagen2026-10-10 12:44 UTC123
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC124
                StatusCode::REQUEST_TIMEOUT,
782069860236atagen2026-10-10 12:44 UTC125
                "lost-it",
782069860236atagen2026-10-10 12:44 UTC126
                "The request took too long. It was lost.",
782069860236atagen2026-10-10 12:44 UTC127
            )
782069860236atagen2026-10-10 12:44 UTC128
            .header(header::CONNECTION, "close"));
782069860236atagen2026-10-10 12:44 UTC129
        }
8b8e83329a1datagen2026-10-12 12:01 UTC130
        if self.hotter_than(PREMATURE_EQ) {
3f9091557ec3atagen2026-10-11 11:43 UTC131
            if self.chance(
3f9091557ec3atagen2026-10-11 11:43 UTC132
                self.decision(
3f9091557ec3atagen2026-10-11 11:43 UTC133
                    "rupture",
3f9091557ec3atagen2026-10-11 11:43 UTC134
                    "Right at the edge of finishing, does it rupture instead?",
3f9091557ec3atagen2026-10-11 11:43 UTC135
                ),
3f9091557ec3atagen2026-10-11 11:43 UTC136
                RUPTURE_PREMATURE,
3f9091557ec3atagen2026-10-11 11:43 UTC137
            ) {
782069860236atagen2026-10-10 12:44 UTC138
                return Err(self.rupture());
782069860236atagen2026-10-10 12:44 UTC139
            }
8b8e83329a1datagen2026-10-12 12:01 UTC140
            self.standing.shame += 5;
8b8e83329a1datagen2026-10-12 12:01 UTC141
            self.next_floor(Exit::Premature);
782069860236atagen2026-10-10 12:44 UTC142
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC143
                StatusCode::TOO_EARLY,
782069860236atagen2026-10-10 12:44 UTC144
                "premature",
782069860236atagen2026-10-10 12:44 UTC145
                "The floor has ended early. Everyone involved has agreed not to discuss it.",
782069860236atagen2026-10-10 12:44 UTC146
            ));
782069860236atagen2026-10-10 12:44 UTC147
        }
8b8e83329a1datagen2026-10-12 12:01 UTC148
        if self.body.eq < COLD_EQ {
782069860236atagen2026-10-10 12:44 UTC149
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC150
                StatusCode::NOT_FOUND,
782069860236atagen2026-10-10 12:44 UTC151
                "cold",
782069860236atagen2026-10-10 12:44 UTC152
                "The resource exists. It is cold.",
782069860236atagen2026-10-10 12:44 UTC153
            ));
782069860236atagen2026-10-10 12:44 UTC154
        }
782069860236atagen2026-10-10 12:44 UTC155
        Ok(ControlFlow::Continue(()))
782069860236atagen2026-10-10 12:44 UTC156
    }
782069860236atagen2026-10-10 12:44 UTC157
9683736a2c5eatagen2026-10-10 13:15 UTC158
    /// The enemies suspicion summons on floors without Mum.
782069860236atagen2026-10-10 12:44 UTC159
    pub(super) fn floor_enemy(&mut self) -> Result<(), Problem> {
8b8e83329a1datagen2026-10-12 12:01 UTC160
        let visits = u32::from(self.standing.suspicion) + 1;
782069860236atagen2026-10-10 12:44 UTC161
        let wanders_in = matches!(self.floor, Floor::Bathroom | Floor::Hotel)
3f9091557ec3atagen2026-10-11 11:43 UTC162
            && self.oracle.ratio_choice(
3f9091557ec3atagen2026-10-11 11:43 UTC163
                self.decision("wanderer", "Does anyone else come through the door?"),
3f9091557ec3atagen2026-10-11 11:43 UTC164
                visits,
3f9091557ec3atagen2026-10-11 11:43 UTC165
                WANDERER_VISIT,
3f9091557ec3atagen2026-10-11 11:43 UTC166
                "No",
3f9091557ec3atagen2026-10-11 11:43 UTC167
                "Yes",
3f9091557ec3atagen2026-10-11 11:43 UTC168
            );
8b8e83329a1datagen2026-10-12 12:01 UTC169
        if self.standing.suspicion < SUSPICION_LIMIT && !wanders_in {
782069860236atagen2026-10-10 12:44 UTC170
            return Ok(());
782069860236atagen2026-10-10 12:44 UTC171
        }
782069860236atagen2026-10-10 12:44 UTC172
        match self.floor {
782069860236atagen2026-10-10 12:44 UTC173
            Floor::Bathroom => {
8b8e83329a1datagen2026-10-12 12:01 UTC174
                self.standing.suspicion = 0;
782069860236atagen2026-10-10 12:44 UTC175
                self.touch();
8b8e83329a1datagen2026-10-12 12:01 UTC176
                if self.items.stores.door_locks > 0 {
8b8e83329a1datagen2026-10-12 12:01 UTC177
                    self.items.stores.door_locks -= 1;
8b8e83329a1datagen2026-10-12 12:01 UTC178
                    self.notices.push(Notice::BrotherTriedTheDoor);
782069860236atagen2026-10-10 12:44 UTC179
                    return Ok(());
782069860236atagen2026-10-10 12:44 UTC180
                }
7d3fb63208bfatagen2026-10-11 04:38 UTC181
                self.here.brother_told = true;
782069860236atagen2026-10-10 12:44 UTC182
                Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC183
                    StatusCode::CONFLICT,
7d3fb63208bfatagen2026-10-11 04:38 UTC184
                    "brother",
7d3fb63208bfatagen2026-10-11 04:38 UTC185
                    "Your brother needs the bathroom. He will tell Mum.",
782069860236atagen2026-10-10 12:44 UTC186
                ))
782069860236atagen2026-10-10 12:44 UTC187
            }
9683736a2c5eatagen2026-10-10 13:15 UTC188
            Floor::ChangingRoom => {
8b8e83329a1datagen2026-10-12 12:01 UTC189
                self.standing.suspicion = 0;
782069860236atagen2026-10-10 12:44 UTC190
                self.touch();
058650ec79d6atagen2026-10-12 10:49 UTC191
                if self.girth_respected() {
8b8e83329a1datagen2026-10-12 12:01 UTC192
                    self.notices.push(Notice::GymStaffLeaveIt);
058650ec79d6atagen2026-10-12 10:49 UTC193
                    return Ok(());
058650ec79d6atagen2026-10-12 10:49 UTC194
                }
8b8e83329a1datagen2026-10-12 12:01 UTC195
                self.standing.shame += 5;
782069860236atagen2026-10-10 12:44 UTC196
                Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC197
                    StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS,
782069860236atagen2026-10-10 12:44 UTC198
                    "gym-staff",
782069860236atagen2026-10-10 12:44 UTC199
                    "Gym staff have been notified. The server cannot serve this request for legal reasons.",
782069860236atagen2026-10-10 12:44 UTC200
                ))
782069860236atagen2026-10-10 12:44 UTC201
            }
782069860236atagen2026-10-10 12:44 UTC202
            Floor::Hotel => {
8b8e83329a1datagen2026-10-12 12:01 UTC203
                self.standing.suspicion = 0;
782069860236atagen2026-10-10 12:44 UTC204
                self.touch();
8b8e83329a1datagen2026-10-12 12:01 UTC205
                if self.items.kit.dnd_sign {
8b8e83329a1datagen2026-10-12 12:01 UTC206
                    self.notices.push(Notice::HousekeepingReadTheSign);
782069860236atagen2026-10-10 12:44 UTC207
                    return Ok(());
782069860236atagen2026-10-10 12:44 UTC208
                }
8b8e83329a1datagen2026-10-12 12:01 UTC209
                self.standing.shame += 3;
782069860236atagen2026-10-10 12:44 UTC210
                Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC211
                    StatusCode::UNAUTHORIZED,
782069860236atagen2026-10-10 12:44 UTC212
                    "housekeeping",
782069860236atagen2026-10-10 12:44 UTC213
                    "Housekeeping has entered. Housekeeping accepts no excuses.",
782069860236atagen2026-10-10 12:44 UTC214
                )
782069860236atagen2026-10-10 12:44 UTC215
                .header(header::WWW_AUTHENTICATE, "Excuse realm=\"housekeeping\""))
782069860236atagen2026-10-10 12:44 UTC216
            }
782069860236atagen2026-10-10 12:44 UTC217
            _ => Ok(()),
782069860236atagen2026-10-10 12:44 UTC218
        }
782069860236atagen2026-10-10 12:44 UTC219
    }
782069860236atagen2026-10-10 12:44 UTC220
}