atagen

jelq

public

an http based roguelike rpg about jelqing

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