atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
4b1f5f88827de40c3cc25edaf1c8acea7192d6cf
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/game/roll.rsRust222 lines7.6 KB
1
use super::{
2
    Grip, Lube, Outcome, Run,
3
    balance::{
4
        ARRIVAL_BONUS, BAND_BONUS, CLEAN_HIT, DANGER_EQ, EQ_BAND, GREED_HOT, GREED_WARM, HOT_EQ,
5
        NEAR_MISS, QUEUED_ETA, RING_RUPTURE_LINE, RUPTURE_LINE, UNDERHAND_STRETCH, UNDERHAND_WET,
6
    },
7
    body::Pending,
8
    measure::round3,
9
    techniques::{Profile, Training},
10
};
11
use crate::{headers::SERVER_TIMING, problem::Problem};
12
use axum::http::{StatusCode, header};
13
use serde_json::json;
14
15
impl Run {
16
    pub(super) fn roll(
17
        &mut self,
18
        training: Training,
19
        intensity: i32,
20
        lube: Option<Lube>,
21
    ) -> Result<Outcome, Problem> {
22
        let profile = training.profile();
23
        let natural: i32 = self.oracle.span_int(
24
            self.decision(
25
                "natural",
26
                "What does the twenty-sided die show for this technique?",
27
            ),
28
            1,
29
            20,
30
        );
31
        let modifier = self.modifier(training, lube);
32
        let dc = profile.dc + intensity / 3;
33
        let total = natural + modifier;
34
        let timing = format!("roll;desc=\"d20{modifier:+} vs {dc}\";dur={natural}");
35
        let timed = |p: Problem| p.header(SERVER_TIMING, &timing);
36
37
        let (length, girth) = self.scaled_gains(&profile, intensity);
38
        let rupture_line = if self.items.kit.ring {
39
            RING_RUPTURE_LINE
40
        } else {
41
            RUPTURE_LINE
42
        };
43
44
        if natural == 1 {
45
            return Err(timed(self.critical_failure(training, &profile)));
46
        }
47
        self.here.warm_arrival = false;
48
        if natural == 2 && self.hotter_than(DANGER_EQ) {
49
            return Err(timed(self.rupture()));
50
        }
51
        if self.hotter_than(rupture_line) && total < dc {
52
            return Err(timed(self.rupture()));
53
        }
54
        if natural == 20 {
55
            self.gain(length * 2.0, girth * 2.0, profile.foreskin_growth);
56
            return Ok(Outcome::json(
57
                StatusCode::PERMANENT_REDIRECT,
58
                json!({
59
                    "detail": "Your length has moved permanently.",
60
                    "length": round3(length * 2.0),
61
                    "girth": round3(girth * 2.0),
62
                }),
63
            )
64
            .header(header::LOCATION, self.path("/length"))
65
            .header(SERVER_TIMING, timing));
66
        }
67
        if total < dc - NEAR_MISS {
68
            return Err(timed(not_extended()));
69
        }
70
        if total < dc {
71
            return Ok(Outcome::empty(StatusCode::NO_CONTENT).header(SERVER_TIMING, timing));
72
        }
73
        Ok(self
74
            .land(&profile, length, girth, total >= dc + CLEAN_HIT)
75
            .header(SERVER_TIMING, timing))
76
    }
77
78
    fn modifier(&self, training: Training, lube: Option<Lube>) -> i32 {
79
        let band = if EQ_BAND.contains(&self.body.eq) {
80
            BAND_BONUS
81
        } else {
82
            0
83
        };
84
        let grip = match (self.body.grip, training) {
85
            (Grip::Underhand, Training::Stretch) => UNDERHAND_STRETCH,
86
            (Grip::Underhand, Training::Wet) => UNDERHAND_WET,
87
            _ => 0,
88
        };
89
        let arrival = if self.here.warm_arrival {
90
            ARRIVAL_BONUS
91
        } else {
92
            0
93
        };
94
        lube.map_or(0, Lube::modifier) + band + grip + arrival
95
    }
96
97
    fn land(&mut self, profile: &Profile, length: f64, girth: f64, clean: bool) -> Outcome {
98
        if self.body.eq < *EQ_BAND.start() {
99
            self.gain(length / 2.0, girth / 2.0, profile.foreskin_growth);
100
            Outcome::json(
101
                StatusCode::PARTIAL_CONTENT,
102
                json!({
103
                    "detail": "Partial gains were applied. EQ was below the band.",
104
                    "length": round3(length / 2.0),
105
                    "girth": round3(girth / 2.0),
106
                }),
107
            )
108
        } else if clean {
109
            self.gain(length, girth, profile.foreskin_growth);
110
            Outcome::json(
111
                StatusCode::OK,
112
                json!({
113
                    "detail": "Gains were applied.",
114
                    "length": round3(length),
115
                    "girth": round3(girth),
116
                }),
117
            )
118
        } else {
119
            let eta = self.oracle.span_int(
120
                self.decision("eta", "In how many turns will the queued gains arrive?"),
121
                *QUEUED_ETA.start(),
122
                *QUEUED_ETA.end(),
123
            );
124
            self.body.pending.push(Pending {
125
                due: self.turn + eta.unsigned_abs(),
126
                length,
127
                girth,
128
                foreskin: profile.foreskin_growth,
129
            });
130
            Outcome::json(
131
                StatusCode::ACCEPTED,
132
                json!({
133
                    "detail": "Gains have been queued. They may arrive.",
134
                    "eta_turns": eta,
135
                }),
136
            )
137
        }
138
    }
139
140
    pub(super) fn scaled_gains(&self, profile: &Profile, intensity: i32) -> (f64, f64) {
141
        let scale = f64::from(intensity);
142
        let (mut length, mut girth) = (profile.length * scale, profile.girth * scale);
143
        let greed = if self.body.eq > HOT_EQ {
144
            GREED_HOT
145
        } else if self.body.eq > DANGER_EQ {
146
            GREED_WARM
147
        } else {
148
            1.0
149
        };
150
        length *= greed;
151
        girth *= greed;
152
        if self.here.teapot {
153
            length *= 0.5;
154
            girth *= 1.5;
155
        }
156
        if self.items.kit.ring {
157
            girth *= 2.0;
158
        }
159
        if self.body.conditions.friction_burn {
160
            length *= 0.5;
161
            girth *= 0.5;
162
        }
163
        (length, girth)
164
    }
165
166
    pub(super) fn critical_failure(&mut self, training: Training, profile: &Profile) -> Problem {
167
        let uses_foreskin = profile.foreskin.is_some_and(|(min, _)| min > 0.0);
168
        if uses_foreskin && self.body.foreskin.is_some() {
169
            self.body.foreskin = None;
170
            self.standing.shame += 25;
171
            return Problem::new(
172
                StatusCode::GONE,
173
                "foreskin-lost",
174
                "The technique has removed the foreskin. It will not be returning.",
175
            )
176
            .header(header::CONTENT_LOCATION, self.path("/foreskin"));
177
        }
178
        if self.hotter_than(DANGER_EQ) {
179
            return self.rupture();
180
        }
181
        if training == Training::Stretch && !self.here.teapot {
182
            self.here.teapot = true;
183
            self.items.kit.teapot_spout = true;
184
            self.standing.shame += 5;
185
            return Problem::new(
186
                StatusCode::IM_A_TEAPOT,
187
                "teapot",
188
                "The subject is now short and stout. Length gains are halved for the rest of the floor.",
189
            );
190
        }
191
        if training == Training::Uli && !self.body.conditions.peyronie {
192
            self.body.conditions.peyronie = true;
193
            self.standing.shame += 5;
194
            return Problem::new(
195
                StatusCode::MISDIRECTED_REQUEST,
196
                "curvature",
197
                "A curvature has developed. Techniques will land slightly to the left until a professional is consulted.",
198
            );
199
        }
200
        not_extended()
201
    }
202
203
    pub(super) fn gossip(&mut self, outcome: Outcome) -> Outcome {
204
        let reportable = matches!(outcome.status().as_u16(), 200 | 202 | 206 | 308);
205
        if !self.here.brother_told || !reportable {
206
            return outcome;
207
        }
208
        self.here.brother_told = false;
209
        outcome.retold(
210
            StatusCode::ALREADY_REPORTED,
211
            "Your brother has already told Mum.",
212
        )
213
    }
214
}
215
216
pub(super) fn not_extended() -> Problem {
217
    Problem::new(
218
        StatusCode::NOT_EXTENDED,
219
        "not-extended",
220
        "The requested extension was not applied. The server has recorded the attempt.",
221
    )
222
}