atagen

jelq

public

an http based roguelike rpg about jelqing

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