atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0

Commit 9374b3933189

tarball

atagen <boss@atagen.co>2026-10-12 11:17 UTC

5 files changed+73-11
game: let girth tighten the ring, fill the jeans and set the price

The extender's 507 checked girth alone, so a long thin run never filled
its jeans even though the extender only adds length. The jeans now hold
girth plus a quarter of the length. Past 5.5 girth the ring is tight
enough to raise its priapism odds to 1 in 4, and under 5 Pump-Bro marks
the girth range up by 3, since he can tell who needs it.
Modified file: src/game/balance.rs+4 additions−1 deletions
@@ -38,7 +38,8 @@ pub const SELF_REPORT_INFLATION: f64 = 1.5;
3838 pub const MM_PER_INCH: f64 = 25.4;
3939 pub const HONEST_TOLERANCE: f64 = 0.06;
4040 pub const EXAGGERATION: f64 = 1.0;
41-pub const STORAGE_GIRTH: f64 = 5.0;
41+/// Girth plus a quarter of the length is what the jeans have to hold alongside the extender.
42+pub const STORAGE_BULK: f64 = 6.5;
4243 pub const GIRTH_RESPECTED: f64 = 5.0;
4344 pub const GIRTH_THICK: f64 = 5.5;
4445 pub const FORESKIN_GLIDE: f64 = 1.0;
@@ -63,6 +64,8 @@ pub const WINDOW: u32 = 60;
6364 pub const PULLED_MUSCLE: u32 = 12;
6465 pub const PULLED_MUSCLE_INTENSITY: i32 = 9;
6566 pub const RING_PRIAPISM: u32 = 10;
67+pub const RING_TIGHT_PRIAPISM: u32 = 4;
68+pub const THIN_MARKUP: u32 = 3;
6669 pub const SUPPLEMENT_WORKS: u32 = 5;
6770 pub const RUPTURE_HOLD: u32 = 250;
6871 pub const ULI_ODDS: u32 = 4;
Modified file: src/game/shop.rs+24 additions−4 deletions
@@ -1,7 +1,7 @@
11 use axum::http::{StatusCode, header};
22 use serde_json::{Value, json};
33
4-use super::{Item, Outcome, Run, items::SHOP};
4+use super::{Item, Outcome, Run, balance::THIN_MARKUP, items::SHOP};
55 use crate::problem::Problem;
66
77 fn pump_bro_absent() -> Problem {
@@ -13,6 +13,20 @@ fn pump_bro_absent() -> Problem {
1313 }
1414
1515 impl Run {
16+ /// Pump-Bro can tell when a customer needs girth, and prices the girth range accordingly.
17+ pub fn price(&self, item: Item) -> Option<u32> {
18+ let girth_range = matches!(
19+ item,
20+ Item::Ring | Item::PumpPro | Item::Supplement | Item::Extender
21+ );
22+ let markup = if girth_range && !self.girth_respected() {
23+ THIN_MARKUP
24+ } else {
25+ 0
26+ };
27+ Some(item.price()? + markup)
28+ }
29+
1630 pub fn shop_view(&self) -> Result<Value, Problem> {
1731 if !self.floor.shop_open() {
1832 return Err(pump_bro_absent());
@@ -22,14 +36,20 @@ impl Run {
2236 .filter_map(|item| {
2337 Some(json!({
2438 "item": item.slug(),
25- "price": item.price()?,
39+ "price": self.price(*item)?,
2640 "description": item.pitch(),
2741 "href": self.path(&format!("/shop/{}", item.slug())),
2842 "method": "POST",
2943 }))
3044 })
3145 .collect();
32- Ok(json!({ "merchant": "Pump-Bro", "stock": stock }))
46+ let mut view = json!({ "merchant": "Pump-Bro", "stock": stock });
47+ if !self.girth_respected() {
48+ view["pitch"] = json!(
49+ "Pump-Bro looks you up and down. The girth range has gone up a little, just for you."
50+ );
51+ }
52+ Ok(view)
3353 }
3454
3555 pub fn buy(&mut self, slug: &str) -> Result<Outcome, Problem> {
@@ -46,7 +66,7 @@ impl Run {
4666 let item = Item::parse(slug)
4767 .filter(|item| SHOP.contains(item))
4868 .ok_or_else(not_for_sale)?;
49- let price = item.price().ok_or_else(not_for_sale)?;
69+ let price = self.price(item).ok_or_else(not_for_sale)?;
5070 if item == Item::Pump {
5171 return Err(Problem::new(
5272 StatusCode::UPGRADE_REQUIRED,
Modified file: src/game/turn.rs+8 additions−3 deletions
@@ -11,7 +11,8 @@ use super::{
1111 FRICTION_DRY, FRICTION_GRIP, FRICTION_HOLD, FRICTION_MAX, FRICTION_MEDITATE, FRICTION_SPIT,
1212 GIRTH_THICK, GREED_HOT, GREED_WARM, HOLD_EQ, HOT_EQ, MEDITATION_EQ, OBSERVED, PRIAPISM_EQ,
1313 PRIAPISM_TURNS, PULLED_MUSCLE, PULLED_MUSCLE_INTENSITY, REFLECTION, RING_PRIAPISM,
14- RING_RUPTURE_LINE, RUPTURE_HOLD, RUPTURE_LINE, STORAGE_GIRTH, SUSPICION_LIMIT,
14+ RING_RUPTURE_LINE, RING_TIGHT_PRIAPISM, RUPTURE_HOLD, RUPTURE_LINE, STORAGE_BULK,
15+ SUSPICION_LIMIT,
1516 },
1617 not_extended, round3,
1718 state::{MumVisit, Observer, Pending},
@@ -321,7 +322,7 @@ impl Run {
321322 }
322323
323324 fn check_storage(&self) -> Result<(), Problem> {
324- if self.items.extender && self.girth > STORAGE_GIRTH {
325+ if self.items.extender && self.girth + self.current_length() / 4.0 > STORAGE_BULK {
325326 return Err(Problem::new(
326327 StatusCode::INSUFFICIENT_STORAGE,
327328 "jeans-full",
@@ -453,7 +454,11 @@ impl Run {
453454 "ring-priapism",
454455 "With the ring still on and everything this warm, will it not go down?",
455456 ),
456- RING_PRIAPISM,
457+ if self.girth > GIRTH_THICK {
458+ RING_TIGHT_PRIAPISM
459+ } else {
460+ RING_PRIAPISM
461+ },
457462 )
458463 {
459464 self.conditions.priapism_turns = PRIAPISM_TURNS;
Modified file: src/http/run.rs+1 additions−1 deletions
@@ -185,7 +185,7 @@ fn links(run: &Run) -> String {
185185 .chain(run.here.shopping.then(|| {
186186 SHOP.iter()
187187 .filter_map(|item| {
188- let price = item.price()?;
188+ let price = run.price(*item)?;
189189 Some(format!(
190190 "<{}>; rel=\"{REL_BASE}shop-item\"; title=\"{} for {price}\"; allow=\"POST\"",
191191 run.path(&format!("/shop/{}", item.slug())),
Modified file: src/tests.rs+36 additions−2 deletions
@@ -13,7 +13,7 @@ use crate::{
1313 AppState, Config,
1414 game::{
1515 Floor, MumVisit, Run, Story,
16- balance::{BRANCH_TURN, STORAGE_GIRTH, SUSPICION_LIMIT},
16+ balance::{BRANCH_TURN, SUSPICION_LIMIT},
1717 },
1818 router,
1919 };
@@ -1638,7 +1638,8 @@ async fn a_refused_jelq_leaves_the_lube_alone() {
16381638 let app = seeded_with(Floor::Bedroom, |run| {
16391639 run.warmed_up = true;
16401640 run.items.extender = true;
1641- run.girth = STORAGE_GIRTH + 1.0;
1641+ run.girth = 6.0;
1642+ run.length = 6.0;
16421643 });
16431644 let lube = [("content-type", "application/baby-oil")];
16441645 let full = send(
@@ -2081,3 +2082,36 @@ async fn the_dead_outlive_the_server() {
20812082 let _ = std::fs::remove_file(format!("{}{suffix}", path.display()));
20822083 }
20832084 }
2085+
2086+#[tokio::test]
2087+async fn pump_bro_charges_the_thin_more_for_girth() {
2088+ for (girth, ring, oil) in [(4.5, 11, 4), (5.2, 8, 4)] {
2089+ let app = seeded_with(Floor::ChangingRoom, |run| run.girth = girth);
2090+ let shop = json(send(&app, "GET", "/runs/cafebabe/shop", &[], "").await).await;
2091+ let price = |slug: &str| {
2092+ shop["stock"]
2093+ .as_array()
2094+ .unwrap()
2095+ .iter()
2096+ .find(|item| item["item"] == slug)
2097+ .unwrap()["price"]
2098+ .as_u64()
2099+ .unwrap()
2100+ };
2101+ assert_eq!(price("cock-ring"), ring, "girth {girth}");
2102+ assert_eq!(price("baby-oil"), oil, "girth {girth}");
2103+ }
2104+}
2105+
2106+#[tokio::test]
2107+async fn a_long_run_fills_the_jeans_before_a_thick_one() {
2108+ let app = seeded_with(Floor::Bedroom, |run| {
2109+ run.warmed_up = true;
2110+ run.items.extender = true;
2111+ run.girth = 4.8;
2112+ run.length = 7.5;
2113+ });
2114+ let lube = [("content-type", "application/baby-oil")];
2115+ let full = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &lube, "").await;
2116+ assert_eq!(full.status(), StatusCode::INSUFFICIENT_STORAGE);
2117+}