atagen

jelq

public

an http based roguelike rpg about jelqing

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