atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0

Commit 4b1f5f88827d

tarball

atagen <boss@atagen.co>2026-10-13 10:20 UTC

11 files changed+84-35
game: let a cold jelq through and make it pay

The 412 forced the refractory period to rewarm the towel, so going cold cost nothing and the towel warmed itself. A cold jelq now goes through with friction and its own 1 in 20 degloving roll, and the towel warms once per floor unless the kettle boils. Dry at intensity 8 rises from 1 in 60 to 1 in 20 to match, since it was already the deliberate push to the limit.
Modified file: src/game/balance/friction.rs+1 additions
@@ -1,6 +1,7 @@
11 pub const FRICTION_MAX: i32 = 10;
22 pub const FRICTION_DRY: i32 = 2;
33 pub const FRICTION_SPIT: i32 = 1;
4+pub const FRICTION_COLD: i32 = 2;
45 pub const FRICTION_GRIP: i32 = 1;
56 pub const FRICTION_HOLD: i32 = 2;
67 pub const FRICTION_MEDITATE: i32 = 2;
Modified file: src/game/balance/mishaps.rs+2 additions−1 deletions
@@ -2,7 +2,8 @@ pub const RUPTURE_IN_BAND: u32 = 250;
22 pub const RUPTURE_WARM: u32 = 25;
33 pub const RUPTURE_HOT: u32 = 10;
44 pub const RUPTURE_PREMATURE: u32 = 3;
5-pub const DEGLOVING: u32 = 60;
5+pub const DEGLOVING: u32 = 20;
6+pub const COLD_DEGLOVING: u32 = 20;
67 pub const DEGLOVING_INTENSITY: i32 = 8;
78 pub const WINDOW: u32 = 60;
89 pub const PULLED_MUSCLE: u32 = 12;
Modified file: src/game/checks.rs+1 additions−9 deletions
@@ -25,14 +25,6 @@ impl Run {
2525 "uli is discussed on the forum, in a thread from 2009, by a man who never posted again. You have not found the reply.",
2626 ));
2727 }
28- if !self.body.warmed_up {
29- return Err(Problem::new(
30- StatusCode::PRECONDITION_FAILED,
31- "cold-start",
32- "Warm-up was skipped. The server has noted this.",
33- )
34- .with("technique", Utility::Warmup.name()));
35- }
3628 if matches!(technique, Technique::Training(_)) {
3729 self.grip_closes(self.body.grip)?;
3830 }
@@ -103,7 +95,7 @@ impl Run {
10395 .filter(move |_| !blocked)
10496 .filter(|&technique| match technique {
10597 Technique::Utility(Utility::Warmup) => {
106- !self.body.warmed_up || self.here.kitchen.kettle_boiled
98+ !self.here.towel_warmed || self.here.kitchen.kettle_boiled
10799 }
108100 Technique::Utility(Utility::Ice) => self.items.stores.ice_packs > 0,
109101 Technique::Utility(Utility::Supplement) => self.items.stores.supplements > 0,
Modified file: src/game/floor.rs+1 additions
@@ -128,6 +128,7 @@ pub struct FloorState {
128128 pub(crate) temp_length: f64,
129129 pub(crate) teapot: bool,
130130 pub(crate) towel_stolen: bool,
131+ pub(crate) towel_warmed: bool,
131132 pub(crate) meditated: bool,
132133 pub(crate) brother_told: bool,
133134 pub(crate) shopping: bool,
Modified file: src/game/hazards.rs+15 additions−3 deletions
@@ -6,9 +6,10 @@ use serde_json::json;
66 use super::{
77 Cause, Floor, Notice, Outcome, Run,
88 balance::{
9- BURN_RUPTURE, COLD_EQ, CURVE_MISSES, DANGER_EQ, DEGLOVING, DEGLOVING_INTENSITY, EQ_BAND,
10- FRIEND, HOT_EQ, LOST_IT_EQ, PREMATURE_EQ, RUPTURE_HOT, RUPTURE_IN_BAND, RUPTURE_PREMATURE,
11- RUPTURE_WARM, SOFT_INTENSITY, SUSPICION_LIMIT, WANDERER_VISIT, WINDOW,
9+ BURN_RUPTURE, COLD_DEGLOVING, COLD_EQ, CURVE_MISSES, DANGER_EQ, DEGLOVING,
10+ DEGLOVING_INTENSITY, EQ_BAND, FRIEND, HOT_EQ, LOST_IT_EQ, PREMATURE_EQ, RUPTURE_HOT,
11+ RUPTURE_IN_BAND, RUPTURE_PREMATURE, RUPTURE_WARM, SOFT_INTENSITY, SUSPICION_LIMIT,
12+ WANDERER_VISIT, WINDOW,
1213 },
1314 techniques::Training,
1415 travel::Exit,
@@ -40,6 +41,17 @@ impl Run {
4041 }
4142
4243 pub(super) fn mishaps(&mut self, training: Training, intensity: i32) -> Interruption {
44+ if !self.body.warmed_up {
45+ if !self.notices.contains(&Notice::ColdStart) {
46+ self.notices.push(Notice::ColdStart);
47+ }
48+ if self.chance(
49+ self.decision("cold-degloving", "Cold and unprepared, does the skin tear?"),
50+ COLD_DEGLOVING,
51+ ) {
52+ return Err(self.end(Cause::Degloved));
53+ }
54+ }
4355 if training == Training::Dry
4456 && intensity >= DEGLOVING_INTENSITY
4557 && self.chance(
Modified file: src/game/notice.rs+2 additions
@@ -12,6 +12,7 @@ pub enum Notice {
1212 ThreadHasReplies,
1313 Reflection,
1414 MumSatisfied,
15+ ColdStart,
1516 }
1617
1718 impl Notice {
@@ -28,6 +29,7 @@ impl Notice {
2829 Self::ThreadHasReplies => "Somewhere, a forum thread from 2009 still has replies.",
2930 Self::Reflection => "the count reached ten, and then the reflection started",
3031 Self::MumSatisfied => "Mum seems satisfied. She takes the washing with her.",
32+ Self::ColdStart => "Warm-up was skipped. The server has noted this.",
3133 }
3234 }
3335 }
Modified file: src/game/progress.rs+7 additions−4 deletions
@@ -2,10 +2,10 @@ use super::{
22 Floor, Lube, MumVisit, Notice, Observer, Run, Story,
33 balance::{
44 BRANCH_TURN, CAT_TAKES_TOWEL, CHANGING_ROOM_CHILL, EQ_WOBBLE, EXTENDER_GAIN,
5- FORESKIN_GLIDE, FRICTION_BURN, FRICTION_BURN_AT, FRICTION_DRY, FRICTION_GRIP, FRICTION_MAX,
6- FRICTION_SPIT, GIRTH_THICK, HOT_EQ, OBSERVED, PRIAPISM_EQ, PRIAPISM_TURNS, PULLED_MUSCLE,
7- PULLED_MUSCLE_INTENSITY, RING_PRIAPISM, RING_TIGHT_PRIAPISM, STROKE_EQ, STROKE_EQ_COST,
8- SUSPICION_LIMIT, VIGOUR,
5+ FORESKIN_GLIDE, FRICTION_BURN, FRICTION_BURN_AT, FRICTION_COLD, FRICTION_DRY,
6+ FRICTION_GRIP, FRICTION_MAX, FRICTION_SPIT, GIRTH_THICK, HOT_EQ, OBSERVED, PRIAPISM_EQ,
7+ PRIAPISM_TURNS, PULLED_MUSCLE, PULLED_MUSCLE_INTENSITY, RING_PRIAPISM, RING_TIGHT_PRIAPISM,
8+ STROKE_EQ, STROKE_EQ_COST, SUSPICION_LIMIT, VIGOUR,
99 },
1010 };
1111
@@ -31,6 +31,9 @@ impl Run {
3131 {
3232 friction -= FRICTION_GRIP;
3333 }
34+ if !self.body.warmed_up {
35+ friction += FRICTION_COLD;
36+ }
3437 self.body.jelq_grip = Some(self.body.grip);
3538 self.body.friction = (self.body.friction + friction).min(FRICTION_MAX);
3639 if self.body.friction >= FRICTION_BURN_AT
Modified file: src/game/utility.rs+4 additions−3 deletions
@@ -19,11 +19,11 @@ const WEEK: Duration = Duration::from_secs(7 * 24 * 60 * 60);
1919
2020 impl Run {
2121 pub(super) fn warm_up(&mut self) -> Result<Outcome, Problem> {
22- if self.body.warmed_up && !self.here.kitchen.kettle_boiled {
22+ if self.here.towel_warmed && !self.here.kitchen.kettle_boiled {
2323 return Err(Problem::new(
2424 StatusCode::CONFLICT,
2525 "towel-cold",
26- "The towel has gone cold. It warms once per floor, or once after the refractory period, unless someone boils the kettle.",
26+ "The towel has gone cold. It warms once per floor, unless someone boils the kettle.",
2727 ));
2828 }
2929 if self.floor == Floor::Hotel
@@ -57,9 +57,10 @@ impl Run {
5757 };
5858 self.touch();
5959 self.step_away();
60- if self.body.warmed_up {
60+ if self.here.towel_warmed {
6161 self.here.kitchen.kettle_boiled = false;
6262 }
63+ self.here.towel_warmed = true;
6364 self.body.warmed_up = true;
6465 self.body.eq = (self.body.eq + boost).min(100);
6566 Ok(Outcome::json(StatusCode::OK, json!({ "detail": detail })))
Modified file: src/tests/methods.rs+19 additions−9 deletions
@@ -28,14 +28,21 @@ async fn bodies_do_not_repeat_the_headers() {
2828 }
2929
3030 #[tokio::test]
31-async fn jelqing_cold_fails_the_precondition() {
32- let Harness { app, .. } = harness(Floor::Bedroom, SEED, |_| {});
33- let response = send(&app, "JELQ", "/runs/cafebabe?technique=stretch", &[], "").await;
34- assert_eq!(response.status(), StatusCode::PRECONDITION_FAILED);
35- assert_eq!(
36- header(&response, "content-type"),
37- "application/problem+json"
38- );
31+async fn jelqing_cold_goes_through_at_a_price() {
32+ let friction = async |warm: bool| {
33+ let app = seeded_with(Floor::Bedroom, |run| {
34+ run.body.warmed_up = warm;
35+ run.body.foreskin = Some(1.0);
36+ });
37+ let response = send(&app, "JELQ", "/runs/cafebabe?technique=stretch", &[], "").await;
38+ let noted = warnings(&response)
39+ .iter()
40+ .any(|w| w.contains("Warm-up was skipped"));
41+ assert_eq!(noted, !warm);
42+ header(&response, "x-friction").to_owned()
43+ };
44+ assert_eq!(friction(true).await, "1/10");
45+ assert_eq!(friction(false).await, "3/10");
3946 }
4047
4148 #[tokio::test]
@@ -119,7 +126,10 @@ async fn http_1_0_gets_the_pdf_treatment() {
119126 async fn links_offer_only_usable_techniques() {
120127 let app = seeded_with(Floor::Bedroom, |run| run.body.foreskin = Some(1.0));
121128 let cold = send(&app, "GET", "/runs/cafebabe", &[], "").await;
122- assert_eq!(advertised(&cold), ["warmup", "ice"]);
129+ assert!(
130+ advertised(&cold).contains(&"stretch".to_owned()),
131+ "a cold jelq is allowed"
132+ );
123133 let warm = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
124134 let usable = advertised(&warm);
125135 assert!(usable.contains(&"wet".to_owned()) && usable.contains(&"dry".to_owned()));
Modified file: src/tests/rolls.rs+28 additions−3 deletions
@@ -160,7 +160,7 @@ async fn the_kitchen_rolls_the_normal_tiers() {
160160 }
161161
162162 #[tokio::test]
163-async fn degloving_takes_one_hard_dry_stroke_in_sixty() {
163+async fn degloving_takes_one_hard_dry_stroke_in_twenty() {
164164 let mut degloved = 0;
165165 for seed in 0..1200u64 {
166166 let app = app_with(Floor::Hotel, seed, |run| {
@@ -187,7 +187,32 @@ async fn degloving_takes_one_hard_dry_stroke_in_sixty() {
187187 }
188188 }
189189 assert!(
190- (8..=35).contains(&degloved),
191- "{degloved} degloved in 1200 hard dry strokes, about 20 expected"
190+ (35..=90).contains(&degloved),
191+ "{degloved} degloved in 1200 hard dry strokes, about 60 expected"
192+ );
193+}
194+
195+#[tokio::test]
196+async fn degloving_takes_one_cold_stroke_in_twenty() {
197+ let mut degloved = 0;
198+ for seed in 0..1200u64 {
199+ let app = app_with(Floor::Hotel, seed, |run| {
200+ run.here.wifi_accepted = true;
201+ run.body.eq = 55;
202+ run.body.foreskin = Some(1.0);
203+ });
204+ let cold = send(&app, "JELQ", "/runs/cafebabe?technique=stretch", &[], "").await;
205+ if cold.status() == StatusCode::GONE
206+ && json(cold).await["type"]
207+ .as_str()
208+ .unwrap()
209+ .ends_with("degloved")
210+ {
211+ degloved += 1;
212+ }
213+ }
214+ assert!(
215+ (35..=90).contains(&degloved),
216+ "{degloved} degloved in 1200 cold strokes, about 60 expected"
192217 );
193218 }
Modified file: src/tests/turns.rs+4 additions−3 deletions
@@ -57,7 +57,7 @@ async fn the_towel_warms_once_until_the_kettle_boils() {
5757 assert_eq!(again.status(), StatusCode::CONFLICT);
5858
5959 let app = seeded_with(Floor::Bedroom, |run| {
60- run.body.warmed_up = true;
60+ run.here.towel_warmed = true;
6161 run.here.kitchen.kettle_boiled = true;
6262 });
6363 let reheated = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
@@ -71,6 +71,7 @@ async fn the_refractory_period_is_the_next_hold() {
7171 let app = seeded_with(Floor::Hotel, |run| {
7272 run.here.wifi_accepted = true;
7373 run.body.warmed_up = true;
74+ run.here.towel_warmed = true;
7475 run.body.eq = 60;
7576 run.body.stamina = 0;
7677 run.body.exhausted = true;
@@ -80,8 +81,8 @@ async fn the_refractory_period_is_the_next_hold() {
8081 let hold = send(&app, "HOLD", "/runs/cafebabe", &[], "").await;
8182 assert_eq!(hold.status(), StatusCode::OK);
8283 assert_eq!(header(&hold, "x-stamina"), "10/10");
83- let cold = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
84- assert_eq!(cold.status(), StatusCode::PRECONDITION_FAILED);
84+ let towel = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
85+ assert_eq!(towel.status(), StatusCode::CONFLICT, "the towel stays cold");
8586 }
8687
8788 #[tokio::test]