atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0

Commit f48daa8adf59

tarball

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

4 files changed+62-11
game: make the refractory period give back only part of the stamina

A refractory HOLD refilled stamina to full, which made running dry nearly free. It now returns 4 to 6, with a 1 in 20 second wind that still fills it.
Modified file: src/game/balance/stamina.rs+2 additions
@@ -2,3 +2,5 @@ pub const MAX_STAMINA: i32 = 10;
22 pub const PULLED_STAMINA: i32 = 7;
33 pub const EDGE_LOOP: u8 = 3;
44 pub const ENERGY_STAMINA: i32 = 3;
5+pub const REFRACTORY_STAMINA: (i32, i32) = (4, 6);
6+pub const SECOND_WIND: u32 = 20;
Modified file: src/game/rest.rs+13 additions−5 deletions
@@ -33,15 +33,23 @@ impl Run {
3333 return Err(self.rupture());
3434 }
3535 if self.body.exhausted {
36- self.recover();
36+ let second_wind = self.recover();
3737 self.shed(FRICTION_HOLD);
3838 self.settle();
39- let detail = if self.body.conditions.priapism_turns > 0 {
40- "The count went well past ten. Stamina is back. It went down a little. Maybe. You hope."
39+ let back = if second_wind {
40+ "The count went well past ten, and something came back that had no business coming back. Stamina is full."
4141 } else {
42- "The count went well past ten. Stamina is back. Everything else has gone quiet, and you are cold again."
42+ "The count went well past ten. Some of the stamina is back."
4343 };
44- return Ok(Outcome::json(StatusCode::OK, json!({ "detail": detail })));
44+ let after = if self.body.conditions.priapism_turns > 0 {
45+ "It went down a little. Maybe. You hope."
46+ } else {
47+ "Everything else has gone quiet, and you are cold again."
48+ };
49+ return Ok(Outcome::json(
50+ StatusCode::OK,
51+ json!({ "detail": format!("{back} {after}") }),
52+ ));
4553 }
4654 if self.chance(
4755 self.decision(
Modified file: src/game/run.rs+26 additions−5 deletions
@@ -10,8 +10,8 @@ use super::{
1010 Body, Cause, Clinic, Ending, Floor, FloorState, Household, Item, Items, Notice, Observer,
1111 Online, RunStatus, Standing, Story,
1212 balance::{
13- BRANCH_TURN, FLOOR_LIMIT, MAX_STAMINA, PRIAPISM_EQ, PULLED_STAMINA, SELF_REPORT_INFLATION,
14- STARTING_BABY_OIL, STARTING_EQ, STARTING_LUBE,
13+ BRANCH_TURN, FLOOR_LIMIT, MAX_STAMINA, PRIAPISM_EQ, PULLED_STAMINA, REFRACTORY_STAMINA,
14+ SECOND_WIND, SELF_REPORT_INFLATION, STARTING_BABY_OIL, STARTING_EQ, STARTING_LUBE,
1515 },
1616 measure::round2,
1717 oracle::{Oracle, OracleTemplate, Roll},
@@ -334,11 +334,32 @@ impl Run {
334334 cause.problem().with("turn", self.turn)
335335 }
336336
337- /// The refractory period is the next HOLD. It comes back with full stamina and nothing else.
338- pub(super) fn recover(&mut self) {
337+ /// The refractory period is the next HOLD. Returns whether it rolled the second wind that refills stamina outright.
338+ pub(super) fn recover(&mut self) -> bool {
339339 self.body.exhausted = false;
340- self.body.stamina = self.max_stamina();
340+ let second_wind = self.chance(
341+ self.decision(
342+ "second-wind",
343+ "Does the refractory period end in a second wind?",
344+ ),
345+ SECOND_WIND,
346+ );
347+ self.body.stamina = if second_wind {
348+ self.max_stamina()
349+ } else {
350+ let (lo, hi) = REFRACTORY_STAMINA;
351+ let back = self.oracle.span_int(
352+ self.decision(
353+ "refractory",
354+ "How much stamina comes back after the refractory period?",
355+ ),
356+ lo,
357+ hi,
358+ );
359+ back.min(self.max_stamina())
360+ };
341361 self.reset_eq(STARTING_EQ);
362+ second_wind
342363 }
343364
344365 pub(super) fn check_exhausted(&self) -> Result<(), Problem> {
Modified file: src/tests/turns.rs+21 additions−1 deletions
@@ -78,7 +78,7 @@ async fn the_refractory_period_is_the_next_hold() {
7878 assert_eq!(tired.status(), StatusCode::TOO_MANY_REQUESTS);
7979 let hold = send(&app, "HOLD", "/runs/cafebabe", &[], "").await;
8080 assert_eq!(hold.status(), StatusCode::OK);
81- assert_eq!(header(&hold, "x-stamina"), "10/10");
81+ assert!(["4/10", "5/10", "6/10", "10/10"].contains(&header(&hold, "x-stamina")));
8282 let towel = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
8383 assert_eq!(towel.status(), StatusCode::CONFLICT, "the towel stays cold");
8484 }
@@ -402,3 +402,23 @@ async fn an_overstayed_jelq_spends_no_lube() {
402402 );
403403 }
404404 }
405+
406+#[tokio::test]
407+async fn the_refractory_period_gives_back_some_stamina_and_rarely_all() {
408+ let mut full = 0;
409+ for seed in 0..400u64 {
410+ let mut run = Run::new("cafebabe".into(), "cafe".into(), seed);
411+ run.floor = Floor::ChangingRoom;
412+ run.body.stamina = 0;
413+ run.body.exhausted = true;
414+ if run.hold().is_err() {
415+ continue;
416+ }
417+ match run.body.stamina {
418+ 4..=6 => {}
419+ 10 => full += 1,
420+ other => panic!("seed {seed} recovered {other} stamina"),
421+ }
422+ }
423+ assert!((8..=35).contains(&full), "{full} second winds in 400");
424+}