atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
1a144ee92b627d87d2ff575eded19a9b8baa3fe1
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/game/hazards.rsRust268 lines9.3 KB
1
use std::{ops::ControlFlow, time::SystemTime};
2
3
use axum::http::{StatusCode, header};
4
use serde_json::json;
5
6
use super::{
7
    Cause, Floor, Notice, Outcome, Run,
8
    balance::{
9
        BURN_RUPTURE, COLD_DEGLOVING, COLD_EQ, CURVE_MISSES, DANGER_EQ, DEGLOVING,
10
        DEGLOVING_INTENSITY, EQ_BAND, FRICTION_MAX, FRICTION_TEAR, FRIEND, HOT_EQ, LOST_IT_EQ,
11
        PREMATURE_EQ, RAW_DEGLOVING, RUPTURE_HOT, RUPTURE_IN_BAND, RUPTURE_PREMATURE, RUPTURE_WARM,
12
        SOFT_INTENSITY, SUSPICION_LIMIT, WANDERER_VISIT, WINDOW,
13
    },
14
    measure::round3,
15
    techniques::Training,
16
    travel::Exit,
17
};
18
use crate::problem::Problem;
19
20
type Interruption = Result<ControlFlow<Outcome>, Problem>;
21
22
impl Run {
23
    /// Whether EQ is past `line`, with a friction burn dragging every line down.
24
    pub(super) fn hotter_than(&self, line: i32) -> bool {
25
        let burn = if self.body.conditions.friction_burn {
26
            BURN_RUPTURE
27
        } else {
28
            0
29
        };
30
        self.body.eq > line - burn
31
    }
32
33
    /// The rupture tier for any turn that is not a jelq, so never the premature band's 1 in 3.
34
    pub(super) fn rupture_odds(&self) -> u32 {
35
        if !self.hotter_than(DANGER_EQ) {
36
            RUPTURE_IN_BAND
37
        } else if !self.hotter_than(HOT_EQ) {
38
            RUPTURE_WARM
39
        } else {
40
            RUPTURE_HOT
41
        }
42
    }
43
44
    pub(super) fn cold(&self) -> bool {
45
        self.body.eq < COLD_EQ
46
    }
47
48
    pub(super) fn mishaps(
49
        &mut self,
50
        training: Training,
51
        intensity: i32,
52
        cold: bool,
53
    ) -> Interruption {
54
        if cold {
55
            if !self.notices.contains(&Notice::ColdStart) {
56
                self.notices.push(Notice::ColdStart);
57
            }
58
            if self.chance(
59
                self.decision("cold-degloving", "Cold and unprepared, does the skin tear?"),
60
                COLD_DEGLOVING,
61
            ) {
62
                return Err(self.deglove());
63
            }
64
        }
65
        if self.body.friction >= FRICTION_MAX {
66
            self.tear()?;
67
        }
68
        if training == Training::Dry
69
            && intensity >= DEGLOVING_INTENSITY
70
            && self.chance(
71
                self.decision("degloving", "Dry at this intensity, does the skin tear?"),
72
                DEGLOVING,
73
            )
74
        {
75
            return Err(self.deglove());
76
        }
77
        Ok(ControlFlow::Continue(()))
78
    }
79
80
    fn tear(&mut self) -> Result<(), Problem> {
81
        if !self.notices.contains(&Notice::SkinGivingWay) {
82
            self.notices.push(Notice::SkinGivingWay);
83
        }
84
        match self.body.foreskin {
85
            Some(left) if left - FRICTION_TEAR > 1e-9 => {
86
                self.body.foreskin = Some(left - FRICTION_TEAR);
87
                self.body.torn += FRICTION_TEAR;
88
                Ok(())
89
            }
90
            Some(left) => {
91
                self.body.foreskin = None;
92
                self.standing.shame += 25;
93
                Err(Problem::new(
94
                    StatusCode::GONE,
95
                    "foreskin-lost",
96
                    "The friction has removed the foreskin. It will not be returning.",
97
                )
98
                .with("foreskin", -round3(left))
99
                .header(header::CONTENT_LOCATION, self.path("/foreskin")))
100
            }
101
            None => {
102
                if self.chance(
103
                    self.decision("raw-degloving", "Rubbed raw, does the skin tear?"),
104
                    RAW_DEGLOVING,
105
                ) {
106
                    return Err(self.deglove());
107
                }
108
                Ok(())
109
            }
110
        }
111
    }
112
113
    fn deglove(&mut self) -> Problem {
114
        let lost = self.body.foreskin.take();
115
        let problem = self.end(Cause::Degloved);
116
        match lost {
117
            Some(lost) => problem.with("foreskin", -round3(lost)),
118
            None => problem,
119
        }
120
    }
121
122
    pub(super) fn hazards(&mut self, intensity: i32) -> Interruption {
123
        if !self.hotter_than(PREMATURE_EQ)
124
            && self.chance(
125
                self.decision("rupture", "Does this jelq rupture something?"),
126
                self.rupture_odds(),
127
            )
128
        {
129
            return Err(self.rupture());
130
        }
131
        if self.floor.has_window()
132
            && !self.items.kit.blinds
133
            && self.oracle.chance_not(
134
                self.decision("window", "Are the curtains still closed?"),
135
                WINDOW,
136
            )
137
        {
138
            self.standing.shame += 30;
139
            self.online.filmed_at.get_or_insert_with(SystemTime::now);
140
            return Ok(ControlFlow::Break(
141
                Outcome::json(
142
                    StatusCode::CREATED,
143
                    json!({ "detail": "A neighbour was at the window. A recording has been created. It has been shared." }),
144
                )
145
                .header(header::LOCATION, self.path("/footage")),
146
            ));
147
        }
148
        if self.floor == Floor::ChangingRoom
149
            && self.chance(
150
                self.decision("friend", "Does a friend walk in and offer to help?"),
151
                FRIEND,
152
            )
153
        {
154
            self.standing.shame += 1;
155
            return Err(Problem::new(
156
                StatusCode::USE_PROXY,
157
                "friend",
158
                "A friend has offered to help. Use Proxy is deprecated, and the server has declined on your behalf.",
159
            ));
160
        }
161
        if self.body.conditions.peyronie
162
            && self.chance(
163
                self.decision(
164
                    "misdirected",
165
                    "With the curve, does the stroke land sharply to the left?",
166
                ),
167
                CURVE_MISSES,
168
            )
169
        {
170
            return Err(Problem::new(
171
                StatusCode::MISDIRECTED_REQUEST,
172
                "misdirected",
173
                "The technique landed slightly to the left. It was meant for a different server.",
174
            ));
175
        }
176
        if intensity <= SOFT_INTENSITY && self.body.eq < *EQ_BAND.start() {
177
            self.body.eq = (self.body.eq - LOST_IT_EQ).max(0);
178
            return Err(Problem::new(
179
                StatusCode::REQUEST_TIMEOUT,
180
                "lost-it",
181
                "The request took too long. It was lost.",
182
            )
183
            .header(header::CONNECTION, "close"));
184
        }
185
        if self.hotter_than(PREMATURE_EQ) {
186
            if self.chance(
187
                self.decision(
188
                    "rupture",
189
                    "Right at the edge of finishing, does it rupture instead?",
190
                ),
191
                RUPTURE_PREMATURE,
192
            ) {
193
                return Err(self.rupture());
194
            }
195
            self.standing.shame += 5;
196
            self.next_floor(Exit::Premature);
197
            return Err(Problem::new(
198
                StatusCode::TOO_EARLY,
199
                "premature",
200
                "The floor has ended early. Everyone involved has agreed not to discuss it.",
201
            ));
202
        }
203
        Ok(ControlFlow::Continue(()))
204
    }
205
206
    /// The enemies suspicion summons on floors without Mum.
207
    pub(super) fn floor_enemy(&mut self) -> Result<(), Problem> {
208
        let visits = u32::from(self.standing.suspicion) + 1;
209
        let wanders_in = matches!(self.floor, Floor::Bathroom | Floor::Hotel)
210
            && self.oracle.ratio_choice(
211
                self.decision("wanderer", "Does anyone else come through the door?"),
212
                visits,
213
                WANDERER_VISIT,
214
                "No",
215
                "Yes",
216
            );
217
        if self.standing.suspicion < SUSPICION_LIMIT && !wanders_in {
218
            return Ok(());
219
        }
220
        match self.floor {
221
            Floor::Bathroom => {
222
                self.standing.suspicion = 0;
223
                self.touch();
224
                if self.items.stores.door_locks > 0 {
225
                    self.items.stores.door_locks -= 1;
226
                    self.notices.push(Notice::BrotherTriedTheDoor);
227
                    return Ok(());
228
                }
229
                self.here.brother_told = true;
230
                Err(Problem::new(
231
                    StatusCode::CONFLICT,
232
                    "brother",
233
                    "Your brother needs the bathroom. He will tell Mum.",
234
                ))
235
            }
236
            Floor::ChangingRoom => {
237
                self.standing.suspicion = 0;
238
                self.touch();
239
                if self.girth_respected() {
240
                    self.notices.push(Notice::GymStaffLeaveIt);
241
                    return Ok(());
242
                }
243
                self.standing.shame += 5;
244
                Err(Problem::new(
245
                    StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS,
246
                    "gym-staff",
247
                    "Gym staff have been notified. The server cannot serve this request for legal reasons.",
248
                ))
249
            }
250
            Floor::Hotel => {
251
                self.standing.suspicion = 0;
252
                self.touch();
253
                if self.items.kit.dnd_sign {
254
                    self.notices.push(Notice::HousekeepingReadTheSign);
255
                    return Ok(());
256
                }
257
                self.standing.shame += 3;
258
                Err(Problem::new(
259
                    StatusCode::UNAUTHORIZED,
260
                    "housekeeping",
261
                    "Housekeeping has entered. Housekeeping accepts no excuses.",
262
                )
263
                .header(header::WWW_AUTHENTICATE, "Excuse realm=\"housekeeping\""))
264
            }
265
            _ => Ok(()),
266
        }
267
    }
268
}