atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
c4d70621d377d45e3d418ac9197443c1b5d36399
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git

Blame: src/game/body.rs

782069860236atagen2026-10-10 12:44 UTC1
use axum::http::StatusCode;
782069860236atagen2026-10-10 12:44 UTC2
use serde::Deserialize;
782069860236atagen2026-10-10 12:44 UTC3
use serde_json::{Value, json};
782069860236atagen2026-10-10 12:44 UTC4
782069860236atagen2026-10-10 12:44 UTC5
use super::{
782069860236atagen2026-10-10 12:44 UTC6
    Grip, Outcome, Run,
c4d70621d377atagen2026-10-12 11:56 UTC7
    balance::{EXAGGERATION, GIRTH_RESPECTED, GIRTH_THICK, LOCKER_ROOM_BAR, PATCH_EQ, PUT_GAIN},
782069860236atagen2026-10-10 12:44 UTC8
    etag_matches, honest,
782069860236atagen2026-10-10 12:44 UTC9
};
782069860236atagen2026-10-10 12:44 UTC10
use crate::problem::Problem;
782069860236atagen2026-10-10 12:44 UTC11
782069860236atagen2026-10-10 12:44 UTC12
#[derive(Deserialize)]
782069860236atagen2026-10-10 12:44 UTC13
pub struct PatchOp {
782069860236atagen2026-10-10 12:44 UTC14
    pub op: String,
782069860236atagen2026-10-10 12:44 UTC15
    pub path: String,
782069860236atagen2026-10-10 12:44 UTC16
    #[serde(default)]
782069860236atagen2026-10-10 12:44 UTC17
    pub value: Value,
782069860236atagen2026-10-10 12:44 UTC18
}
782069860236atagen2026-10-10 12:44 UTC19
782069860236atagen2026-10-10 12:44 UTC20
pub(super) fn exaggeration() -> Problem {
782069860236atagen2026-10-10 12:44 UTC21
    Problem::new(
782069860236atagen2026-10-10 12:44 UTC22
        StatusCode::BAD_REQUEST,
782069860236atagen2026-10-10 12:44 UTC23
        "exaggeration",
782069860236atagen2026-10-10 12:44 UTC24
        "The server has measured. Lying is not a technique.",
782069860236atagen2026-10-10 12:44 UTC25
    )
782069860236atagen2026-10-10 12:44 UTC26
}
782069860236atagen2026-10-10 12:44 UTC27
782069860236atagen2026-10-10 12:44 UTC28
impl Run {
782069860236atagen2026-10-10 12:44 UTC29
    pub fn put(&mut self) -> Outcome {
782069860236atagen2026-10-10 12:44 UTC30
        if self.put_applied {
782069860236atagen2026-10-10 12:44 UTC31
            return Outcome::empty(StatusCode::NO_CONTENT);
782069860236atagen2026-10-10 12:44 UTC32
        }
782069860236atagen2026-10-10 12:44 UTC33
        self.put_applied = true;
782069860236atagen2026-10-10 12:44 UTC34
        self.length += PUT_GAIN;
782069860236atagen2026-10-10 12:44 UTC35
        self.touch();
782069860236atagen2026-10-10 12:44 UTC36
        Outcome::json(StatusCode::OK, json!({ "detail": "Applied." }))
782069860236atagen2026-10-10 12:44 UTC37
    }
782069860236atagen2026-10-10 12:44 UTC38
c4d70621d377atagen2026-10-12 11:56 UTC39
    /// Being measured is not arousing, so every PATCH costs a little EQ whatever it finds.
782069860236atagen2026-10-10 12:44 UTC40
    pub fn patch(&mut self, ops: &[PatchOp]) -> Result<Outcome, Problem> {
c4d70621d377atagen2026-10-12 11:56 UTC41
        self.reset_eq(self.eq - PATCH_EQ);
c4d70621d377atagen2026-10-12 11:56 UTC42
        self.touch();
782069860236atagen2026-10-10 12:44 UTC43
        for op in ops {
782069860236atagen2026-10-10 12:44 UTC44
            if op.op != "test" {
782069860236atagen2026-10-10 12:44 UTC45
                return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC46
                    StatusCode::UNPROCESSABLE_ENTITY,
782069860236atagen2026-10-10 12:44 UTC47
                    "wishful-thinking",
782069860236atagen2026-10-10 12:44 UTC48
                    "The server accepts only the truth.",
782069860236atagen2026-10-10 12:44 UTC49
                )
782069860236atagen2026-10-10 12:44 UTC50
                .with("op", op.op.clone()));
782069860236atagen2026-10-10 12:44 UTC51
            }
782069860236atagen2026-10-10 12:44 UTC52
            let actual = match op.path.as_str() {
782069860236atagen2026-10-10 12:44 UTC53
                "/length" => self.current_length(),
782069860236atagen2026-10-10 12:44 UTC54
                "/girth" => self.girth,
782069860236atagen2026-10-10 12:44 UTC55
                "/foreskin" => self.foreskin.unwrap_or(0.0),
782069860236atagen2026-10-10 12:44 UTC56
                _ => {
782069860236atagen2026-10-10 12:44 UTC57
                    return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC58
                        StatusCode::UNPROCESSABLE_ENTITY,
782069860236atagen2026-10-10 12:44 UTC59
                        "no-such-measurement",
782069860236atagen2026-10-10 12:44 UTC60
                        "There is no such measurement.",
782069860236atagen2026-10-10 12:44 UTC61
                    )
782069860236atagen2026-10-10 12:44 UTC62
                    .with("path", op.path.clone()));
782069860236atagen2026-10-10 12:44 UTC63
                }
782069860236atagen2026-10-10 12:44 UTC64
            };
782069860236atagen2026-10-10 12:44 UTC65
            let Some(claimed) = op.value.as_f64() else {
782069860236atagen2026-10-10 12:44 UTC66
                return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC67
                    StatusCode::BAD_REQUEST,
782069860236atagen2026-10-10 12:44 UTC68
                    "malformed-patch",
782069860236atagen2026-10-10 12:44 UTC69
                    "A test requires a numeric value.",
782069860236atagen2026-10-10 12:44 UTC70
                ));
782069860236atagen2026-10-10 12:44 UTC71
            };
782069860236atagen2026-10-10 12:44 UTC72
            if op.path == "/length" && claimed >= actual + EXAGGERATION {
782069860236atagen2026-10-10 12:44 UTC73
                self.shame += 2;
782069860236atagen2026-10-10 12:44 UTC74
                self.touch();
782069860236atagen2026-10-10 12:44 UTC75
                return Err(exaggeration());
782069860236atagen2026-10-10 12:44 UTC76
            }
782069860236atagen2026-10-10 12:44 UTC77
            if !honest(claimed, actual) {
782069860236atagen2026-10-10 12:44 UTC78
                self.shame += 1;
782069860236atagen2026-10-10 12:44 UTC79
                self.touch();
782069860236atagen2026-10-10 12:44 UTC80
                return Err(
782069860236atagen2026-10-10 12:44 UTC81
                    Problem::new(StatusCode::CONFLICT, "test-failed", "The test failed.")
782069860236atagen2026-10-10 12:44 UTC82
                        .with("path", op.path.clone()),
782069860236atagen2026-10-10 12:44 UTC83
                );
782069860236atagen2026-10-10 12:44 UTC84
            }
782069860236atagen2026-10-10 12:44 UTC85
        }
782069860236atagen2026-10-10 12:44 UTC86
        Ok(Outcome::json(
782069860236atagen2026-10-10 12:44 UTC87
            StatusCode::OK,
782069860236atagen2026-10-10 12:44 UTC88
            json!({ "detail": "Correct." }),
782069860236atagen2026-10-10 12:44 UTC89
        ))
782069860236atagen2026-10-10 12:44 UTC90
    }
782069860236atagen2026-10-10 12:44 UTC91
782069860236atagen2026-10-10 12:44 UTC92
    pub fn delete_foreskin(&mut self, if_match: Option<&str>) -> Result<Outcome, Problem> {
782069860236atagen2026-10-10 12:44 UTC93
        if self.foreskin.is_none() {
782069860236atagen2026-10-10 12:44 UTC94
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC95
                StatusCode::GONE,
782069860236atagen2026-10-10 12:44 UTC96
                "foreskin-gone",
782069860236atagen2026-10-10 12:44 UTC97
                "The foreskin is gone. It will not be returning.",
782069860236atagen2026-10-10 12:44 UTC98
            ));
782069860236atagen2026-10-10 12:44 UTC99
        }
782069860236atagen2026-10-10 12:44 UTC100
        let Some(if_match) = if_match else {
782069860236atagen2026-10-10 12:44 UTC101
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC102
                StatusCode::PRECONDITION_REQUIRED,
782069860236atagen2026-10-10 12:44 UTC103
                "confirmation-required",
782069860236atagen2026-10-10 12:44 UTC104
                "The server requires you to be sure. Send If-Match.",
782069860236atagen2026-10-10 12:44 UTC105
            ));
782069860236atagen2026-10-10 12:44 UTC106
        };
782069860236atagen2026-10-10 12:44 UTC107
        if !etag_matches(if_match, &self.etag()) {
782069860236atagen2026-10-10 12:44 UTC108
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC109
                StatusCode::PRECONDITION_FAILED,
782069860236atagen2026-10-10 12:44 UTC110
                "stale-foreskin",
782069860236atagen2026-10-10 12:44 UTC111
                "The foreskin has changed since you last looked at it.",
782069860236atagen2026-10-10 12:44 UTC112
            ));
782069860236atagen2026-10-10 12:44 UTC113
        }
782069860236atagen2026-10-10 12:44 UTC114
        self.foreskin = None;
782069860236atagen2026-10-10 12:44 UTC115
        self.shame += 20;
782069860236atagen2026-10-10 12:44 UTC116
        self.touch();
782069860236atagen2026-10-10 12:44 UTC117
        Ok(Outcome::empty(StatusCode::NO_CONTENT))
782069860236atagen2026-10-10 12:44 UTC118
    }
782069860236atagen2026-10-10 12:44 UTC119
058650ec79d6atagen2026-10-12 10:49 UTC120
    pub fn set_grip(&mut self, grip: Grip) -> Result<(), Problem> {
058650ec79d6atagen2026-10-12 10:49 UTC121
        self.grip_closes(grip)?;
782069860236atagen2026-10-10 12:44 UTC122
        self.grip = grip;
782069860236atagen2026-10-10 12:44 UTC123
        self.touch();
058650ec79d6atagen2026-10-12 10:49 UTC124
        Ok(())
058650ec79d6atagen2026-10-12 10:49 UTC125
    }
058650ec79d6atagen2026-10-12 10:49 UTC126
058650ec79d6atagen2026-10-12 10:49 UTC127
    pub(super) fn grip_closes(&self, grip: Grip) -> Result<(), Problem> {
058650ec79d6atagen2026-10-12 10:49 UTC128
        if grip == Grip::Underhand && self.girth > GIRTH_THICK {
058650ec79d6atagen2026-10-12 10:49 UTC129
            return Err(Problem::new(
058650ec79d6atagen2026-10-12 10:49 UTC130
                StatusCode::PAYLOAD_TOO_LARGE,
058650ec79d6atagen2026-10-12 10:49 UTC131
                "grip-too-small",
058650ec79d6atagen2026-10-12 10:49 UTC132
                format!(
058650ec79d6atagen2026-10-12 10:49 UTC133
                    "The underhand grip no longer closes around {:.2} inches of girth.",
058650ec79d6atagen2026-10-12 10:49 UTC134
                    self.girth
058650ec79d6atagen2026-10-12 10:49 UTC135
                ),
058650ec79d6atagen2026-10-12 10:49 UTC136
            )
058650ec79d6atagen2026-10-12 10:49 UTC137
            .with("maximum", GIRTH_THICK));
058650ec79d6atagen2026-10-12 10:49 UTC138
        }
058650ec79d6atagen2026-10-12 10:49 UTC139
        Ok(())
058650ec79d6atagen2026-10-12 10:49 UTC140
    }
058650ec79d6atagen2026-10-12 10:49 UTC141
058650ec79d6atagen2026-10-12 10:49 UTC142
    /// Everyone in the changing room self-reports, so the comparison is self-report against self-report.
058650ec79d6atagen2026-10-12 10:49 UTC143
    pub fn holds_own_in_the_locker_room(&self) -> bool {
058650ec79d6atagen2026-10-12 10:49 UTC144
        self.self_reported() >= LOCKER_ROOM_BAR
058650ec79d6atagen2026-10-12 10:49 UTC145
    }
058650ec79d6atagen2026-10-12 10:49 UTC146
058650ec79d6atagen2026-10-12 10:49 UTC147
    pub fn girth_respected(&self) -> bool {
058650ec79d6atagen2026-10-12 10:49 UTC148
        self.girth >= GIRTH_RESPECTED
058650ec79d6atagen2026-10-12 10:49 UTC149
    }
159f0998263batagen2026-10-12 11:01 UTC150
}
058650ec79d6atagen2026-10-12 10:49 UTC151
159f0998263batagen2026-10-12 11:01 UTC152
/// A cylinder whose circumference is the girth.
159f0998263batagen2026-10-12 11:01 UTC153
pub fn volume(length: f64, girth: f64) -> f64 {
159f0998263batagen2026-10-12 11:01 UTC154
    girth * girth * length / (4.0 * std::f64::consts::PI)
782069860236atagen2026-10-10 12:44 UTC155
}