atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
b85a0d4536c3b294547a682ac046d8a38beb47a3
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/game/mod.rsRust344 lines9.4 KB
1
pub mod balance;
2
mod body;
3
mod clinic;
4
mod floors;
5
mod forum;
6
mod hazards;
7
mod household;
8
mod items;
9
mod kitchen;
10
mod outcome;
11
mod shop;
12
mod state;
13
mod techniques;
14
mod turn;
15
mod utility;
16
17
pub use body::PatchOp;
18
pub use forum::{ELDER_LAST_MODIFIED, FORUM_THREAD, forum_thread};
19
pub use items::{Item, Items, SHOP};
20
pub use outcome::{Outcome, Payload};
21
pub use state::{
22
    Cause, Clinic, Conditions, Elder, Ending, Floor, FloorState, Grip, MumVisit, Observer, Story,
23
};
24
pub use techniques::{LUBRICANTS, Lube, Technique};
25
26
use std::{
27
    hash::{DefaultHasher, Hash, Hasher},
28
    time::{Duration, Instant, SystemTime},
29
};
30
31
use axum::http::{StatusCode, header};
32
use rand::{Rng, SeedableRng, rngs::StdRng};
33
use serde_json::{Value, json};
34
35
use crate::problem::Problem;
36
use balance::{
37
    HONEST_TOLERANCE, MAX_STAMINA, PRIAPISM_EQ, PULLED_STAMINA, SELF_REPORT_INFLATION,
38
    STARTING_CASH, STARTING_EQ,
39
};
40
use state::Pending;
41
42
pub struct Run {
43
    /// The private id in the run's URL. Knowing it is the only authorisation a run has.
44
    pub id: String,
45
    pub public_id: String,
46
    etag_salt: u64,
47
    pub version: u64,
48
    pub turn: u32,
49
    pub floor: Floor,
50
    pub here: FloorState,
51
    pub length: f64,
52
    pub girth: f64,
53
    pub foreskin: Option<f64>,
54
    pub eq: i32,
55
    pub stamina: i32,
56
    pub suspicion: u8,
57
    pub shame: u32,
58
    pub shame_reported: u32,
59
    pub shame_total: u32,
60
    pub cash: u32,
61
    pub grip: Grip,
62
    pub friction: i32,
63
    pub jelq_grip: Option<Grip>,
64
    pub items: Items,
65
    pub conditions: Conditions,
66
    pub warmed_up: bool,
67
    pub put_applied: bool,
68
    pub story: Story,
69
    pub uli_taught: bool,
70
    pub kitchen_tellings: u8,
71
    pub mum: Option<MumVisit>,
72
    pub heard: Vec<String>,
73
    pub elder: Elder,
74
    pub clinic: Clinic,
75
    pub seeded: bool,
76
    pub filmed_at: Option<SystemTime>,
77
    pub observed: Option<Observer>,
78
    pub refractory: Duration,
79
    pub refractory_until: Option<Instant>,
80
    pub pending: Vec<Pending>,
81
    pub notices: Vec<&'static str>,
82
    pub ended: Option<Ending>,
83
    pub last_seen: Instant,
84
    rng: StdRng,
85
}
86
87
pub fn round2(x: f64) -> f64 {
88
    (x * 100.0).round() / 100.0
89
}
90
91
fn round3(x: f64) -> f64 {
92
    (x * 1000.0).round() / 1000.0
93
}
94
95
fn honest(claimed: f64, actual: f64) -> bool {
96
    (claimed - actual).abs() < HONEST_TOLERANCE
97
}
98
99
fn not_extended() -> Problem {
100
    Problem::new(
101
        StatusCode::NOT_EXTENDED,
102
        "not-extended",
103
        "The requested extension was not applied. The server has recorded the attempt.",
104
    )
105
}
106
107
pub fn parse_credentials<'a>(authorization: &'a str, scheme: &str) -> Option<&'a str> {
108
    let (given, rest) = authorization.trim().split_once(' ')?;
109
    given
110
        .eq_ignore_ascii_case(scheme)
111
        .then(|| rest.trim().trim_matches('"'))
112
}
113
114
pub fn etag_matches(condition: &str, etag: &str) -> bool {
115
    condition
116
        .split(',')
117
        .map(str::trim)
118
        .any(|tag| tag == "*" || tag.trim_start_matches("W/") == etag)
119
}
120
121
pub fn shaft(mm: usize) -> String {
122
    format!("8{}D", "=".repeat(mm.saturating_sub(2)))
123
}
124
125
impl Run {
126
    pub fn new(id: String, public_id: String, seed: u64) -> Self {
127
        let mut rng = StdRng::seed_from_u64(seed);
128
        let length = round2(rng.random_range(4.5..6.5));
129
        let girth = round2(rng.random_range(4.0..5.0));
130
        let foreskin = round2(rng.random_range(0.4..2.4));
131
        let etag_salt = rng.random();
132
        Self {
133
            id,
134
            public_id,
135
            etag_salt,
136
            version: 0,
137
            turn: 0,
138
            floor: Floor::Bedroom,
139
            here: FloorState::default(),
140
            length,
141
            girth,
142
            foreskin: Some(foreskin),
143
            eq: STARTING_EQ,
144
            stamina: MAX_STAMINA,
145
            suspicion: 0,
146
            shame: 0,
147
            shame_reported: 0,
148
            shame_total: 0,
149
            cash: STARTING_CASH,
150
            grip: Grip::Overhand,
151
            friction: 0,
152
            jelq_grip: None,
153
            items: Items::default(),
154
            conditions: Conditions::default(),
155
            warmed_up: false,
156
            put_applied: false,
157
            story: Story::Unopened,
158
            uli_taught: false,
159
            kitchen_tellings: 0,
160
            mum: None,
161
            heard: Vec::new(),
162
            notices: Vec::new(),
163
            elder: Elder::Unverified { strikes: 0 },
164
            clinic: Clinic::default(),
165
            seeded: false,
166
            filmed_at: None,
167
            observed: None,
168
            refractory: Duration::from_secs(30),
169
            refractory_until: None,
170
            pending: Vec::new(),
171
            ended: None,
172
            last_seen: Instant::now(),
173
            rng,
174
        }
175
    }
176
177
    fn etag_at(&self, version: u64) -> String {
178
        let mut hasher = DefaultHasher::new();
179
        (self.etag_salt, version).hash(&mut hasher);
180
        format!("\"{:016x}\"", hasher.finish())
181
    }
182
183
    pub fn etag(&self) -> String {
184
        self.etag_at(self.version)
185
    }
186
187
    /// The ETag the run had when it was created, which the true ending asks for.
188
    pub fn origin_etag(&self) -> String {
189
        self.etag_at(0)
190
    }
191
192
    pub fn touch(&mut self) {
193
        self.version += 1;
194
    }
195
196
    pub(super) fn shed(&mut self, friction: i32) {
197
        self.friction = (self.friction - friction).max(0);
198
    }
199
200
    /// Priapism holds EQ up no matter what else moves it.
201
    pub(super) fn reset_eq(&mut self, eq: i32) {
202
        let floor = if self.conditions.priapism_turns > 0 {
203
            PRIAPISM_EQ
204
        } else {
205
            0
206
        };
207
        self.eq = eq.max(floor);
208
    }
209
210
    fn fresh_nonce(&mut self) -> String {
211
        format!("{:016x}", self.rng.random::<u64>())
212
    }
213
214
    fn chance(&mut self, odds: u32) -> bool {
215
        self.rng.random_ratio(1, odds)
216
    }
217
218
    pub fn path(&self, sub: &str) -> String {
219
        format!("/runs/{}{sub}", self.id)
220
    }
221
222
    pub fn current_length(&self) -> f64 {
223
        self.length + self.here.temp_length
224
    }
225
226
    pub fn self_reported(&self) -> f64 {
227
        self.current_length() + SELF_REPORT_INFLATION
228
    }
229
230
    pub fn max_stamina(&self) -> i32 {
231
        if self.conditions.pulled_muscle {
232
            PULLED_STAMINA
233
        } else {
234
            MAX_STAMINA
235
        }
236
    }
237
238
    pub fn forum_read(&self) -> bool {
239
        self.story == Story::ReadForum
240
    }
241
242
    pub fn footage(&self) -> Option<(SystemTime, u64)> {
243
        let filmed = self.filmed_at?;
244
        let age = filmed.elapsed().map_or(0, |d| d.as_secs());
245
        Some((filmed, age))
246
    }
247
248
    pub fn status(&self) -> &'static str {
249
        match (&self.ended, self.clinic.cured) {
250
            (Some(_), _) => "gone",
251
            (None, true) => "normal",
252
            (None, false) => "alive",
253
        }
254
    }
255
256
    /// The parts of the run that its response headers do not already carry.
257
    pub fn view(&self) -> Value {
258
        let conditions: Vec<&str> = [
259
            (self.here.teapot, "short and stout"),
260
            (self.conditions.peyronie, "curved"),
261
            (self.items.cage, "caged"),
262
            (self.conditions.priapism_turns > 0, "priapism"),
263
            (self.conditions.pulled_muscle, "pulled muscle"),
264
            (self.filmed_at.is_some(), "viral"),
265
        ]
266
        .into_iter()
267
        .filter_map(|(active, name)| active.then_some(name))
268
        .collect();
269
        json!({
270
            "public_id": self.public_id,
271
            "status": self.status(),
272
            "turn": self.turn,
273
            "run_shame": self.shame,
274
            "grip": self.grip.name(),
275
            "warmed_up": self.warmed_up,
276
            "ranked": !self.seeded,
277
            "conditions": conditions,
278
        })
279
    }
280
281
    pub fn end(&mut self, cause: Cause) -> Problem {
282
        self.ended = Some(Ending {
283
            cause,
284
            turn: self.turn,
285
        });
286
        cause.problem().with("turn", self.turn)
287
    }
288
289
    /// Ends the refractory period once it has run out, whatever the request.
290
    pub fn recover(&mut self, now: Instant) {
291
        if self.refractory_until.is_some_and(|until| now >= until) {
292
            self.refractory_until = None;
293
            self.stamina = self.max_stamina();
294
            self.reset_eq(STARTING_EQ);
295
            self.warmed_up = false;
296
            self.touch();
297
        }
298
    }
299
300
    fn check_refractory(&mut self, now: Instant) -> Result<(), Problem> {
301
        self.recover(now);
302
        let Some(until) = self.refractory_until else {
303
            return Ok(());
304
        };
305
        let seconds = until.saturating_duration_since(now).as_secs() + 1;
306
        Err(Problem::new(
307
            StatusCode::TOO_MANY_REQUESTS,
308
            "overtrained",
309
            "Stamina is exhausted. The refractory period is in effect.",
310
        )
311
        .header(header::RETRY_AFTER, seconds.to_string()))
312
    }
313
314
    fn exhaust(&mut self, now: Instant) {
315
        self.stamina = 0;
316
        self.refractory_until = Some(now + self.refractory);
317
    }
318
319
    fn realise(&mut self) {
320
        let (due, waiting): (Vec<_>, Vec<_>) = std::mem::take(&mut self.pending)
321
            .into_iter()
322
            .partition(|p| p.due <= self.turn);
323
        self.pending = waiting;
324
        for p in due {
325
            self.length += p.length;
326
            self.girth += p.girth;
327
        }
328
    }
329
330
    fn gain(&mut self, length: f64, girth: f64, foreskin: f64) {
331
        self.length += length;
332
        self.girth += girth;
333
        if let Some(f) = &mut self.foreskin {
334
            *f += foreskin;
335
        }
336
    }
337
338
    fn rupture(&mut self) -> Problem {
339
        let cause = Cause::Ruptured {
340
            ice_unused: self.items.ice_packs > 0,
341
        };
342
        self.end(cause)
343
    }
344
}